@orderlyshop/web-components 0.1.0-build.7066 → 0.1.0-build.7067

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.
Files changed (69) hide show
  1. package/AGENTS.md +15 -4
  2. package/README.md +167 -32
  3. package/bin/orderly-generate-server-renderers.mjs +69 -11
  4. package/bin/orderly-hydrate-static-pages.mjs +47 -13
  5. package/bin/orderly-init-shop.mjs +50 -7
  6. package/custom-elements.json +34 -9
  7. package/dist/browser/orderly-web-components.define.global.js +585 -120
  8. package/dist/browser/orderly-web-components.define.global.js.br +0 -0
  9. package/dist/browser/orderly-web-components.define.global.js.gz +0 -0
  10. package/dist/browser/orderly-web-components.define.global.js.map +1 -1
  11. package/dist/browser/orderly-web-components.global.js +585 -120
  12. package/dist/browser/orderly-web-components.global.js.br +0 -0
  13. package/dist/browser/orderly-web-components.global.js.gz +0 -0
  14. package/dist/browser/orderly-web-components.global.js.map +1 -1
  15. package/dist/{default-shop-BDktbMzl.d.ts → default-shop-CQuz1DKx.d.ts} +6 -2
  16. package/dist/default-shop.d.ts +2 -2
  17. package/dist/default-shop.js +1 -905
  18. package/dist/default-shop.js.br +0 -0
  19. package/dist/default-shop.js.gz +0 -0
  20. package/dist/default-shop.js.map +1 -1
  21. package/dist/define-DThgfT4n.d.ts +10 -0
  22. package/dist/define.d.ts +1 -1
  23. package/dist/define.js +721 -9933
  24. package/dist/define.js.br +0 -0
  25. package/dist/define.js.gz +0 -0
  26. package/dist/define.js.map +1 -1
  27. package/dist/index.d.ts +37 -9
  28. package/dist/index.js +650 -10698
  29. package/dist/index.js.br +0 -0
  30. package/dist/index.js.gz +0 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/navigation.d.ts +10 -2
  33. package/dist/navigation.js +9 -1357
  34. package/dist/navigation.js.br +0 -0
  35. package/dist/navigation.js.gz +0 -0
  36. package/dist/navigation.js.map +1 -1
  37. package/dist/query.js +1 -114
  38. package/dist/query.js.br +0 -0
  39. package/dist/query.js.gz +0 -0
  40. package/dist/query.js.map +1 -1
  41. package/dist/shop-CYr-svkP.d.ts +313 -0
  42. package/dist/shop-query.js +1 -99
  43. package/dist/shop-query.js.br +0 -0
  44. package/dist/shop-query.js.gz +0 -0
  45. package/dist/shop-query.js.map +1 -1
  46. package/dist/shop.d.ts +3 -3
  47. package/dist/shop.js +773 -10506
  48. package/dist/shop.js.br +0 -0
  49. package/dist/shop.js.gz +0 -0
  50. package/dist/shop.js.map +1 -1
  51. package/dist/stores.d.ts +1 -1
  52. package/dist/stores.js +1 -375
  53. package/dist/stores.js.br +0 -0
  54. package/dist/stores.js.gz +0 -0
  55. package/dist/stores.js.map +1 -1
  56. package/dist/taxonomy.d.ts +2 -2
  57. package/dist/taxonomy.js +1 -246
  58. package/dist/taxonomy.js.br +0 -0
  59. package/dist/taxonomy.js.gz +0 -0
  60. package/dist/taxonomy.js.map +1 -1
  61. package/dist/{types-CXEwL2xS.d.ts → types-CeigwmOf.d.ts} +7 -1
  62. package/docs/components/README.md +74 -5
  63. package/docs/shop-best-practices.md +128 -0
  64. package/html-custom-data.json +28 -5
  65. package/package.json +3 -3
  66. package/server/README.md +2 -0
  67. package/server/php/orderly-product.php +25 -2
  68. package/dist/define-BgDPJcdv.d.ts +0 -9
  69. package/dist/shop-MI-N1X7L.d.ts +0 -153
@@ -197,14 +197,14 @@ function renderCategoryFallback(category, products) {
197
197
  <section slot="content-before" class="orderly-ssr-category" data-orderly-ssr-fallback data-orderly-category="${escapeAttribute(category.id)}">
198
198
  ${renderBreadcrumb(category)}
199
199
  <header class="orderly-ssr-category__hero">
200
- ${category.metadata.heroImage ? `<img src="${escapeAttribute(category.metadata.heroImage)}" alt="" loading="eager">` : ""}
200
+ ${category.metadata.heroImage ? `<img src="${escapeAttribute(category.metadata.heroImage)}" alt="" loading="eager" fetchpriority="high" decoding="async">` : ""}
201
201
  <div class="orderly-ssr-category__text">
202
202
  <p class="orderly-ssr-category__eyebrow">${escapeHtml(categoryLabelPath(category).join(" / "))}</p>
203
203
  <h1 data-orderly-field="title">${escapeHtml(category.label)}</h1>
204
204
  <p data-orderly-field="description">${escapeHtml(category.metadata.description)}</p>
205
205
  </div>
206
206
  </header>
207
- ${renderProducts(products, "orderly-ssr-product-grid")}
207
+ ${renderProducts(products, "orderly-ssr-product-grid", { priorityFirst: !category.metadata.heroImage })}
208
208
  </section>
209
209
  `;
210
210
  }
@@ -231,20 +231,20 @@ function categoryLabelPath(category) {
231
231
  });
232
232
  }
233
233
 
234
- function renderProducts(products, className) {
234
+ function renderProducts(products, className, options = {}) {
235
235
  if (products.length === 0) {
236
236
  return "";
237
237
  }
238
238
  return `
239
239
  <div class="${className}" data-orderly-products>
240
- ${products.map(renderProduct).join("\n")}
240
+ ${products.map((product, index) => renderProduct(product, { priority: Boolean(options.priorityFirst) && index === 0 })).join("\n")}
241
241
  </div>`;
242
242
  }
243
243
 
244
- function renderProduct(product) {
244
+ function renderProduct(product, options = {}) {
245
245
  const title = product.Title || "Produkt";
246
246
  const brand = normalizedBrand(product.Brand);
247
- const image = firstImageUrl(product);
247
+ const image = firstImageUrl(product, { width: 500, height: 500 });
248
248
  const href = productHref(product);
249
249
  const description = product.Description?.trim();
250
250
  const sku = product.SKU?.Value?.trim();
@@ -256,7 +256,7 @@ function renderProduct(product) {
256
256
  ${sku ? `<meta itemprop="sku" content="${escapeAttribute(sku)}">` : ""}
257
257
  ${href ? `<a class="orderly-ssr-product__link" href="${escapeAttribute(href)}">` : ""}
258
258
  <div class="orderly-ssr-product__image">
259
- ${image ? `<img src="${escapeAttribute(image)}" alt="${escapeAttribute(title)}" loading="lazy">` : ""}
259
+ ${image ? `<img src="${escapeAttribute(image)}" alt="${escapeAttribute(title)}" ${ssrImageAttributes(options.priority)}>` : ""}
260
260
  </div>
261
261
  <div class="orderly-ssr-product__text">
262
262
  <h3 data-orderly-field="title" itemprop="name">${escapeHtml(title)}</h3>
@@ -267,6 +267,12 @@ function renderProduct(product) {
267
267
  </article>`;
268
268
  }
269
269
 
270
+ function ssrImageAttributes(priority) {
271
+ return priority
272
+ ? `loading="eager" fetchpriority="high" decoding="async"`
273
+ : `loading="lazy" decoding="async"`;
274
+ }
275
+
270
276
  function renderOffer(product, href) {
271
277
  const price = schemaPrice(product.Price);
272
278
  const currency = schemaCurrency(product.Price);
@@ -301,11 +307,11 @@ function compactProductShareUrl(url) {
301
307
  return url.replace(/^https:\/\/orderly\.shop\//i, "");
302
308
  }
303
309
 
304
- function firstImageUrl(product) {
305
- return storedImageUrl(product.Images?.[0]);
310
+ function firstImageUrl(product, options = {}) {
311
+ return storedImageUrl(product.Images?.[0], options);
306
312
  }
307
313
 
308
- function storedImageUrl(image) {
314
+ function storedImageUrl(image, options = {}) {
309
315
  if (!image) {
310
316
  return "";
311
317
  }
@@ -313,14 +319,14 @@ function storedImageUrl(image) {
313
319
  for (const key of ["URL", "Url", "url", "ThumbnailUrl", "thumbnailUrl"]) {
314
320
  const value = candidate[key];
315
321
  if (typeof value === "string" && value.length > 0) {
316
- return value;
322
+ return withImageSizeParams(value, options);
317
323
  }
318
324
  }
319
325
  if (image.ImageDataBase64) {
320
326
  return `data:image/jpeg;base64,${image.ImageDataBase64}`;
321
327
  }
322
328
  if (/^https?:\/\//i.test(image.Name) || image.Name.startsWith("/")) {
323
- return image.Name;
329
+ return withImageSizeParams(image.Name, options);
324
330
  }
325
331
  if (!image.Name) {
326
332
  return "";
@@ -329,7 +335,35 @@ function storedImageUrl(image) {
329
335
  const path = image.IsAsset || imageName.includes("cms") || imageName.startsWith("images")
330
336
  ? "objects"
331
337
  : "thumbnails";
332
- return joinUrl(imageBaseUrl, path, image.Name);
338
+ return withImageSizeParams(joinUrl(imageBaseUrl, path, image.Name), options);
339
+ }
340
+
341
+ function withImageSizeParams(url, options = {}) {
342
+ if (/^data:/i.test(url)) {
343
+ return url;
344
+ }
345
+ const width = imageUrlDimension(options.width);
346
+ const height = imageUrlDimension(options.height);
347
+ if (!width && !height) {
348
+ return url;
349
+ }
350
+ const hashIndex = url.indexOf("#");
351
+ const hash = hashIndex >= 0 ? url.slice(hashIndex) : "";
352
+ const withoutHash = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
353
+ const queryIndex = withoutHash.indexOf("?");
354
+ const path = queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash;
355
+ const search = new URLSearchParams(queryIndex >= 0 ? withoutHash.slice(queryIndex + 1) : "");
356
+ if (width) {
357
+ search.set("width", String(width));
358
+ }
359
+ if (height) {
360
+ search.set("height", String(height));
361
+ }
362
+ return `${path}?${search.toString()}${hash}`;
363
+ }
364
+
365
+ function imageUrlDimension(value) {
366
+ return value !== undefined && Number.isFinite(value) && value > 0 ? Math.ceil(value) : undefined;
333
367
  }
334
368
 
335
369
  function formatCredit(credit) {
@@ -419,6 +419,7 @@ function headIncludeSource(options) {
419
419
 
420
420
  const categoryUrlMode = import.meta.env.VITE_ORDERLY_CATEGORY_URL_MODE === "hash" ? "hash" : "path";
421
421
  const productUrlMode = import.meta.env.VITE_ORDERLY_PRODUCT_URL_MODE === "hash" ? "hash" : "path";
422
+ const googleTagManagerId = import.meta.env.VITE_ORDERLY_GOOGLE_TAG_MANAGER_ID?.trim();
422
423
  const productHref = productUrlMode === "hash" ? "/product.html" : "/products/";
423
424
 
424
425
  configureShop({
@@ -440,14 +441,46 @@ function headIncludeSource(options) {
440
441
  logoSrc: "https://orderly.shop/home/App_Icon.svg",
441
442
  logoAlt: ${JSON.stringify(options.shopName)},
442
443
  logoHref: "/"
443
- }
444
+ },
445
+ consent: googleTagManagerId ? {
446
+ enabled: true,
447
+ policyHref: "/information/cookies/",
448
+ managedCookies: [
449
+ {
450
+ name: "_ga",
451
+ category: "statistics",
452
+ provider: "Google Analytics",
453
+ purpose: "Skelner mellem besøgende i statistikmåling.",
454
+ duration: "Typisk op til 2 år"
455
+ },
456
+ {
457
+ name: "_ga_*",
458
+ category: "statistics",
459
+ provider: "Google Analytics",
460
+ purpose: "Bevarer sessionsstatus for statistikmåling.",
461
+ duration: "Typisk op til 2 år"
462
+ },
463
+ {
464
+ name: "_gcl_au",
465
+ category: "marketing",
466
+ provider: "Google",
467
+ purpose: "Måler annonce- og konverteringseffekt.",
468
+ duration: "Typisk op til 90 dage"
469
+ }
470
+ ]
471
+ } : undefined,
472
+ analytics: googleTagManagerId ? {
473
+ googleTagManagerId,
474
+ defaultCurrency: "DKK",
475
+ affiliation: ${JSON.stringify(options.shopName)}
476
+ } : undefined
444
477
  });
445
478
  </script>
446
479
  `;
447
480
  }
448
481
 
449
482
  function bodyStartSource() {
450
- return `<!-- Add shared body-start snippets here, for example Google Tag Manager noscript fallback. -->
483
+ return `<!-- Add shared body-start snippets here, for example consent markup or a manually pasted Google Tag Manager noscript fallback. -->
451
484
  <!-- orderly-include-template: ../templates/shop-footer.html -->
452
485
  <!-- orderly-include-template: ../templates/product-tile.html -->
453
486
  <!-- orderly-include-template: ../templates/product-page.html -->
@@ -715,17 +748,27 @@ function shopFooterTemplateSource(options) {
715
748
 
716
749
  function productTileTemplateSource() {
717
750
  return `<template data-orderly-template="product" data-orderly-for="product-tile">
718
- <article class="orderly-product-tile">
719
- <orderly-stored-image class="orderly-product-tile__image" data-orderly-bind="image" fit="contain"></orderly-stored-image>
751
+ <article class="orderly-product-tile" data-orderly-product>
752
+ <orderly-stored-image class="orderly-product-tile__image" data-orderly-bind="image" fit="contain" image-role="product-tile"></orderly-stored-image>
753
+ <link data-orderly-bind="schema-url" data-orderly-schema-prop="url">
754
+ <link data-orderly-bind="schema-image" data-orderly-schema-prop="image">
755
+ <meta data-orderly-bind="schema-description" data-orderly-schema-prop="description">
756
+ <meta data-orderly-bind="schema-sku" data-orderly-schema-prop="sku">
720
757
  <div class="orderly-product-tile__body">
721
758
  <h3 class="orderly-product-tile__title">
722
759
  <a class="orderly-product-tile__link" href="/product.html" data-orderly-bind="share-url">
723
760
  <span data-orderly-bind="title"></span>
724
761
  </a>
725
762
  </h3>
726
- <p class="orderly-product-tile__brand" data-orderly-bind="brand"></p>
763
+ <p class="orderly-product-tile__brand" data-orderly-schema-brand><span data-orderly-bind="brand"></span></p>
727
764
  <div class="orderly-product-tile__footer">
728
- <p class="orderly-product-tile__price" data-orderly-bind="price"></p>
765
+ <div class="orderly-product-tile__offer" data-orderly-schema-offer>
766
+ <link data-orderly-bind="schema-url" data-orderly-schema-prop="url">
767
+ <meta data-orderly-bind="schema-price-currency" data-orderly-schema-prop="priceCurrency">
768
+ <meta data-orderly-bind="schema-price" data-orderly-schema-prop="price">
769
+ <link data-orderly-bind="schema-availability" data-orderly-schema-prop="availability">
770
+ <p class="orderly-product-tile__price" data-orderly-bind="price"></p>
771
+ </div>
729
772
  <button class="orderly-product-tile__add" type="button" data-orderly-action="add-to-basket">
730
773
  <span class="orderly-product-tile__add-icon" data-orderly-bind="basket-action-icon" aria-hidden="true"></span>
731
774
  </button>
@@ -740,7 +783,7 @@ function productPageTemplateSource() {
740
783
  return `<template data-orderly-template="product" data-orderly-for="product-page">
741
784
  <section class="orderly-product-page">
742
785
  <div class="orderly-product-page__media" style="display: grid; gap: 12px; align-content: start; justify-self: center; width: 100%; max-width: 520px; min-width: 0;">
743
- <orderly-stored-image class="orderly-product-page__image" data-orderly-bind="image" fit="contain" variant="object" style="display: block; width: 100%; max-width: 520px; min-width: 0; aspect-ratio: 1 / 1; overflow: hidden;"></orderly-stored-image>
786
+ <orderly-stored-image class="orderly-product-page__image" data-orderly-bind="image" fit="contain" variant="object" image-role="product-detail" style="display: block; width: 100%; max-width: 520px; min-width: 0; aspect-ratio: 1 / 1; overflow: hidden;"></orderly-stored-image>
744
787
  <div class="orderly-product-page__thumbnails" data-orderly-slot="thumbnails"></div>
745
788
  </div>
746
789
  <div class="orderly-product-page__details" style="min-width: 0;">
@@ -56,7 +56,8 @@
56
56
  { "name": "checkout-href", "description": "Checkout page URL used by the basket." },
57
57
  { "name": "checkout-label", "description": "Checkout link label." },
58
58
  { "name": "search-placeholder", "description": "Search box placeholder." },
59
- { "name": "empty-label", "description": "Empty result text." }
59
+ { "name": "empty-label", "description": "Empty result text." },
60
+ { "name": "basket-mode", "description": "Basket display mode. Use inline-desktop to show a package-owned right rail on desktop when the basket has lines." }
60
61
  ],
61
62
  "members": [
62
63
  { "kind": "field", "name": "client", "type": { "text": "OrderlyClient | undefined" } },
@@ -83,7 +84,8 @@
83
84
  { "name": "empty-label", "description": "Empty result text used by category rails." },
84
85
  { "name": "product-href", "description": "Product detail page URL used by the popup expand link." },
85
86
  { "name": "checkout-href", "description": "Checkout page URL used by the basket." },
86
- { "name": "checkout-label", "description": "Checkout link label." }
87
+ { "name": "checkout-label", "description": "Checkout link label." },
88
+ { "name": "basket-mode", "description": "Basket display mode. Use inline-desktop to show a package-owned right rail on desktop when the basket has lines." }
87
89
  ],
88
90
  "members": [
89
91
  { "kind": "field", "name": "client", "type": { "text": "OrderlyClient | undefined" } },
@@ -150,6 +152,9 @@
150
152
  { "name": "reference-label", "description": "Order reference label." },
151
153
  { "name": "order-title", "description": "Order summary heading." },
152
154
  { "name": "empty-label", "description": "Text shown when the order is not found locally." }
155
+ ],
156
+ "events": [
157
+ { "name": "orderly-payment-success", "type": { "text": "CustomEvent<{ order: Order }>" } }
153
158
  ]
154
159
  },
155
160
  {
@@ -167,6 +172,9 @@
167
172
  { "name": "order-title", "description": "Order summary heading." },
168
173
  { "name": "empty-label", "description": "Text shown when the order is not found locally." },
169
174
  { "name": "retry-label", "description": "Retry payment link label." }
175
+ ],
176
+ "events": [
177
+ { "name": "orderly-payment-failure", "type": { "text": "CustomEvent<{ order: Order }>" } }
170
178
  ]
171
179
  },
172
180
  {
@@ -263,6 +271,8 @@
263
271
  { "name": "title", "description": "Collection heading." },
264
272
  { "name": "description", "description": "Collection description." },
265
273
  { "name": "hero-image", "description": "Hero image URL." },
274
+ { "name": "hero-layout", "description": "Hero layout variant: image, split, or covers." },
275
+ { "name": "hero-cover-count", "description": "Number of product cover images shown in split/covers hero layouts." },
266
276
  { "name": "cta-label", "description": "CTA link label." },
267
277
  { "name": "cta-href", "description": "CTA link URL." },
268
278
  { "name": "keywords", "description": "Declarative query text mapped to SearchQuery.Query." },
@@ -295,7 +305,8 @@
295
305
  { "name": "add-label", "description": "Add-to-basket button label." },
296
306
  { "name": "loading-label", "description": "Product loading text." },
297
307
  { "name": "not-found-label", "description": "Product not found text." },
298
- { "name": "error-label", "description": "Product loading error text." }
308
+ { "name": "error-label", "description": "Product loading error text." },
309
+ { "name": "basket-mode", "description": "Basket display mode. Use inline-desktop to show a package-owned right rail on desktop when the basket has lines." }
299
310
  ],
300
311
  "members": [
301
312
  { "kind": "field", "name": "client", "type": { "text": "OrderlyClient | undefined" } },
@@ -362,6 +373,7 @@
362
373
  ],
363
374
  "events": [
364
375
  { "name": "orderly-add-to-basket", "type": { "text": "CustomEvent<ProductEventDetail>" } },
376
+ { "name": "orderly-product-viewed", "type": { "text": "CustomEvent<ProductEventDetail>" } },
365
377
  { "name": "orderly-product-image-selected", "type": { "text": "CustomEvent<{ product?: SearchObject; image?: StoredImage; index: number }>" } }
366
378
  ]
367
379
  },
@@ -370,11 +382,15 @@
370
382
  "name": "OrderlyStoredImageElement",
371
383
  "tagName": "orderly-stored-image",
372
384
  "customElement": true,
373
- "description": "Renders StoredImage with configured URL resolution, rotation, crop, and fit behavior. Borderless by default, with an opt-in border attribute.",
385
+ "description": "Renders StoredImage with configured URL resolution, role-based CDN sizing, rotation, crop, and fit behavior. Borderless by default, with an opt-in border attribute.",
374
386
  "attributes": [
375
387
  { "name": "fit", "description": "Image fit, normally contain or cover." },
376
388
  { "name": "variant", "description": "Image URL variant, such as thumbnail or object." },
389
+ { "name": "image-role", "description": "Configured image sizing role, such as product-tile, product-detail, thumbnail, basket-line, checkout-line, or fullsize." },
377
390
  { "name": "alt", "description": "Image alt text." },
391
+ { "name": "loading", "description": "Native image loading hint forwarded to the inner img. Defaults to lazy." },
392
+ { "name": "decoding", "description": "Native image decoding hint forwarded to the inner img. Defaults to async." },
393
+ { "name": "fetchpriority", "description": "Native fetch priority hint forwarded to the inner img, for above-the-fold images." },
378
394
  { "name": "border", "description": "Adds a 1px image frame border." }
379
395
  ],
380
396
  "members": [
@@ -409,14 +425,18 @@
409
425
  "customElement": true,
410
426
  "description": "Basket icon button with count badge.",
411
427
  "attributes": [
412
- { "name": "label", "description": "Accessible basket label." }
428
+ { "name": "label", "description": "Accessible basket label." },
429
+ { "name": "href", "description": "Optional checkout/cart URL. When set, the icon renders as a link instead of an open-basket button." },
430
+ { "name": "placement", "description": "Use mobile-fixed for a package-owned fixed mobile cart/checkout button." },
431
+ { "name": "hide-when-empty", "description": "Hide the icon while the assigned BasketController count is zero." }
413
432
  ],
414
433
  "members": [
415
434
  { "kind": "field", "name": "basketController", "type": { "text": "BasketController" } }
416
435
  ],
417
436
  "events": [
418
437
  { "name": "orderly-basket-open", "type": { "text": "CustomEvent<DraftOrder>" } },
419
- { "name": "orderly-basket-change", "type": { "text": "CustomEvent<DraftOrder>" } }
438
+ { "name": "orderly-basket-change", "type": { "text": "CustomEvent<DraftOrder>" } },
439
+ { "name": "orderly-basket-checkout", "type": { "text": "CustomEvent<{ draft: DraftOrder; count: number; href: string }>" } }
420
440
  ]
421
441
  },
422
442
  {
@@ -443,7 +463,8 @@
443
463
  ],
444
464
  "events": [
445
465
  { "name": "orderly-basket-change", "type": { "text": "CustomEvent<DraftOrder>" } },
446
- { "name": "orderly-basket-verified", "type": { "text": "CustomEvent<DraftOrder>" } }
466
+ { "name": "orderly-basket-verified", "type": { "text": "CustomEvent<DraftOrder>" } },
467
+ { "name": "orderly-basket-checkout", "type": { "text": "CustomEvent<{ draft: DraftOrder; count: number; href: string }>" } }
447
468
  ]
448
469
  },
449
470
  {
@@ -483,7 +504,9 @@
483
504
  { "name": "layout", "description": "Navigation layout. Use vertical for a side menu, horizontal for a tiered desktop menu with a scrolling top row, full-width second row, and third-level dropdowns, or burgermenu for an icon trigger that opens a nested disclosure menu inside a floating panel." },
484
505
  { "name": "layout-desktop", "description": "Desktop layout override. Use layout-desktop=\"horizontal\" with layout-mobile=\"burgermenu\" for the standard storefront desktop nav plus mobile header burger pattern." },
485
506
  { "name": "layout-mobile", "description": "Mobile layout override. Use burgermenu for a compact icon button inside a mobile header." },
486
- { "name": "sticky", "description": "Set to false to opt out of sticky positioning." }
507
+ { "name": "sticky", "description": "Set to false to opt out of sticky positioning." },
508
+ { "name": "persist-state", "description": "Persist expanded category ids in localStorage so vertical menus can remember open branches across navigation." },
509
+ { "name": "state-key", "description": "Storage key suffix used with persist-state when a shop has multiple independent menus." }
487
510
  ],
488
511
  "members": [
489
512
  { "kind": "field", "name": "items", "type": { "text": "NavigationItem[]" } },
@@ -497,8 +520,10 @@
497
520
  { "name": "orderly-navigation-select", "type": { "text": "CustomEvent<NavigationSelectionDetail>" } }
498
521
  ],
499
522
  "slots": [
523
+ { "name": "menu-before", "description": "Menu-level content rendered before navigation items, for example store links or service promos." },
500
524
  { "name": "before-items", "description": "Inline content rendered before the navigation items, for example logos." },
501
- { "name": "after-items", "description": "Inline content rendered after the navigation items, for example promotional blocks." }
525
+ { "name": "after-items", "description": "Inline content rendered after the navigation items, for example promotional blocks." },
526
+ { "name": "menu-after", "description": "Menu-level content rendered after navigation items, for example help, store finder, or volunteer links." }
502
527
  ]
503
528
  },
504
529
  {