@akinon/pz-theme 2.0.37-rc.0 → 2.0.37

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 (78) hide show
  1. package/CHANGELOG.md +7 -31
  2. package/package.json +4 -4
  3. package/src/__tests__/__snapshots__/facet-filters-block.test.tsx.snap +11 -0
  4. package/src/__tests__/custom-css.test.ts +181 -0
  5. package/src/__tests__/designer-chrome-clipping.test.tsx +497 -0
  6. package/src/__tests__/facet-filters-block.test.tsx +707 -0
  7. package/src/__tests__/item-template.test.tsx +184 -0
  8. package/src/__tests__/listing-controls.test.tsx +961 -0
  9. package/src/__tests__/part-style-scope.test.tsx +229 -0
  10. package/src/__tests__/part-styles.test.ts +311 -0
  11. package/src/__tests__/registry-parity.test.ts +42 -0
  12. package/src/__tests__/row-visibility.test.ts +672 -0
  13. package/src/__tests__/sort-select-page-cursor.test.tsx +336 -0
  14. package/src/__tests__/theme-block-html-id.test.tsx +167 -0
  15. package/src/blocks/__tests__/__snapshots__/product-gallery-block.test.tsx.snap +15 -0
  16. package/src/blocks/__tests__/accordion-block-state.test.tsx +201 -0
  17. package/src/blocks/__tests__/facet-filters-block.test.tsx +190 -0
  18. package/src/blocks/__tests__/listing-context-card-model.test.tsx +230 -0
  19. package/src/blocks/__tests__/listing-lookup-hardening.test.ts +80 -0
  20. package/src/blocks/__tests__/product-gallery-block.test.tsx +974 -0
  21. package/src/blocks/__tests__/product-grid-composition.test.tsx +570 -0
  22. package/src/blocks/__tests__/product-grid-lookups.test.tsx +230 -0
  23. package/src/blocks/__tests__/product-price-block.test.tsx +248 -0
  24. package/src/blocks/__tests__/product-recommendations-block.test.tsx +702 -0
  25. package/src/blocks/__tests__/share-block.test.tsx +392 -0
  26. package/src/blocks/__tests__/store-locator-block.test.tsx +691 -0
  27. package/src/blocks/__tests__/variant-selector-block.test.tsx +817 -0
  28. package/src/blocks/__tests__/variant-selector-golden.json +20 -0
  29. package/src/blocks/account-menu-block.tsx +264 -0
  30. package/src/blocks/add-to-cart-block.tsx +417 -0
  31. package/src/blocks/address-list-block.tsx +348 -0
  32. package/src/blocks/applied-filters-block.tsx +288 -0
  33. package/src/blocks/auth-form-block.tsx +295 -0
  34. package/src/blocks/basket-items-block.tsx +457 -0
  35. package/src/blocks/basket-summary-block.tsx +404 -0
  36. package/src/blocks/block-renderer-registry.tsx +94 -0
  37. package/src/blocks/change-password-form-block.tsx +240 -0
  38. package/src/blocks/checkout-steps-block.tsx +288 -0
  39. package/src/blocks/checkout-summary-block.tsx +449 -0
  40. package/src/blocks/countdown-block.tsx +182 -0
  41. package/src/blocks/coupon-list-block.tsx +292 -0
  42. package/src/blocks/facet-filters-block.tsx +1686 -0
  43. package/src/blocks/faq-accordion-block.tsx +358 -0
  44. package/src/blocks/favorite-button-block.tsx +252 -0
  45. package/src/blocks/favourites-grid-block.tsx +510 -0
  46. package/src/blocks/forgot-password-block.tsx +155 -0
  47. package/src/blocks/item-template-utils.ts +131 -0
  48. package/src/blocks/item-template.tsx +164 -0
  49. package/src/blocks/listing-breadcrumb-block.tsx +175 -0
  50. package/src/blocks/listing-context.ts +850 -0
  51. package/src/blocks/listing-pagination-block.tsx +923 -0
  52. package/src/blocks/listing-result-count-block.tsx +126 -0
  53. package/src/blocks/listing-title-block.tsx +115 -0
  54. package/src/blocks/oauth-buttons-block.tsx +205 -0
  55. package/src/blocks/order-detail-block.tsx +474 -0
  56. package/src/blocks/order-list-block.tsx +274 -0
  57. package/src/blocks/order-success-block.tsx +641 -0
  58. package/src/blocks/order-tracking-block.tsx +533 -0
  59. package/src/blocks/product-attributes-block.tsx +307 -0
  60. package/src/blocks/product-block-utils.tsx +639 -0
  61. package/src/blocks/product-breadcrumb-block.tsx +215 -0
  62. package/src/blocks/product-description-block.tsx +169 -0
  63. package/src/blocks/product-gallery-block.tsx +1523 -0
  64. package/src/blocks/product-grid-block.tsx +966 -0
  65. package/src/blocks/product-price-block.tsx +373 -0
  66. package/src/blocks/product-recommendations-block.tsx +1433 -0
  67. package/src/blocks/product-title-block.tsx +129 -0
  68. package/src/blocks/profile-form-block.tsx +297 -0
  69. package/src/blocks/recently-viewed-store.ts +201 -0
  70. package/src/blocks/register-form-block.tsx +362 -0
  71. package/src/blocks/share-block.tsx +667 -0
  72. package/src/blocks/size-guide-block.tsx +313 -0
  73. package/src/blocks/sort-select-block.tsx +636 -0
  74. package/src/blocks/stock-status-block.tsx +168 -0
  75. package/src/blocks/store-locator-block.tsx +1184 -0
  76. package/src/blocks/variant-selector-block.tsx +1110 -0
  77. package/src/sections/__tests__/tablet-raw-bucket-readers.test.tsx +186 -0
  78. package/src/utils/__tests__/part-ownership-guard.test.tsx +29 -4
package/CHANGELOG.md CHANGED
@@ -1,36 +1,12 @@
1
1
  # @akinon/pz-theme
2
2
 
3
- ## 2.0.37-rc.0
4
-
5
- ### Patch Changes
6
-
7
- - Updated dependencies [0cf9ea239]
8
- - Updated dependencies [324f97d55]
9
- - Updated dependencies [51ea0688]
10
- - Updated dependencies
11
- - Updated dependencies [2fea4143]
12
- - Updated dependencies [b55acb768]
13
- - Updated dependencies [760258c1c]
14
- - Updated dependencies [143be2b9d]
15
- - Updated dependencies [7889b08fe]
16
- - Updated dependencies [9f8cd3bc5]
17
- - Updated dependencies [bfafa3f49]
18
- - Updated dependencies [57d7eb30]
19
- - Updated dependencies [d99a6a7d5]
20
- - Updated dependencies [9db81a71]
21
- - Updated dependencies [591e345e1]
22
- - Updated dependencies [4de5303c5]
23
- - Updated dependencies [95b139dc1]
24
- - Updated dependencies [1d00f2d0]
25
- - Updated dependencies [4ac7b2a1e]
26
- - Updated dependencies [e9598c71]
27
- - Updated dependencies [4998a9631]
28
- - Updated dependencies [804d2bd6]
29
- - Updated dependencies [3909d3224]
30
- - Updated dependencies [6a3d8a63]
31
- - Updated dependencies [e18836b20]
32
- - @akinon/next@2.0.37-rc.0
33
- - @akinon/theme-contract@2.0.37-rc.0
3
+ ## 2.0.37
4
+
5
+ ### Patch Changes
6
+
7
+ - 9fc516ac: ZERO-4705: Add the commerce block and section vocabulary — product, listing, basket, account, auth and content renderers a designer can place, with their content still commerce-driven
8
+ - @akinon/next@2.0.37
9
+ - @akinon/theme-contract@2.0.37
34
10
 
35
11
  ## 2.0.36
36
12
 
package/package.json CHANGED
@@ -1,26 +1,26 @@
1
1
  {
2
2
  "name": "@akinon/pz-theme",
3
3
  "description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
4
- "version": "2.0.37-rc.0",
4
+ "version": "2.0.37",
5
5
  "license": "MIT",
6
6
  "main": "src/index.ts",
7
7
  "scripts": {
8
8
  "test": "jest"
9
9
  },
10
10
  "peerDependencies": {
11
- "@akinon/next": "2.0.37-rc.0",
11
+ "@akinon/next": "2.0.37",
12
12
  "react": "^18.0.0 || ^19.0.0",
13
13
  "react-dom": "^18.0.0 || ^19.0.0"
14
14
  },
15
15
  "dependencies": {
16
- "@akinon/theme-contract": "2.0.37-rc.0",
16
+ "@akinon/theme-contract": "2.0.37",
17
17
  "clsx": "2.1.1",
18
18
  "lottie-web": "^5.13.0",
19
19
  "tailwind-merge": "^2.5.4",
20
20
  "zod": "4.4.1"
21
21
  },
22
22
  "devDependencies": {
23
- "@akinon/next": "2.0.37-rc.0",
23
+ "@akinon/next": "2.0.37",
24
24
  "@testing-library/jest-dom": "5.16.4",
25
25
  "@testing-library/react": "16.3.2",
26
26
  "@testing-library/user-event": "14.1.1",
@@ -0,0 +1,11 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`default render is frozen renders an opened dropdown unchanged 1`] = `"<div data-facet-filters="" data-facet-layout="dropdown" data-part="list" style="display: flex; flex-direction: row; flex-wrap: wrap; gap: var(--facet-filters-gap, 16px);"><div data-facet-group="color" data-part="group" style="min-width: 180px; position: relative;" data-state="open"><button type="button" data-facet-toggle="" aria-expanded="true" data-part="label" style="display: flex; align-items: center; justify-content: space-between; width: 100%; gap: 8px; background: none; cursor: pointer; font-weight: 600; border-style: solid; border-width: 1px; padding: 10px 12px;" data-state="open"><span>Renk</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" data-part="icon" style="transform: rotate(180deg); transition: transform 150ms;" data-state="open"><path d="M6 9l6 6 6-6"></path></svg></button><ul data-facet-choices="" data-part="panel" data-state="open" style="list-style: none; margin: 0px; padding: 12px; display: flex; flex-direction: column; flex-wrap: nowrap; gap: 6px; position: absolute; z-index: 20; top: calc(100% + 4px); left: 0px; min-width: 100%; max-height: 320px; overflow-y: auto; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);"><li><button type="button" data-facet-choice="black" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>Siyah</span><span data-part="count" style="opacity: 0.6;">(12)</span></button></li><li><button type="button" data-facet-choice="white" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>Beyaz</span><span data-part="count" style="opacity: 0.6;">(7)</span></button></li><li><button type="button" data-facet-choice="navy" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>Lacivert</span><span data-part="count" style="opacity: 0.6;">(3)</span></button></li></ul></div><div data-facet-group="size" data-part="group" style="min-width: 180px; position: relative;"><button type="button" data-facet-toggle="" aria-expanded="false" data-part="label" style="display: flex; align-items: center; justify-content: space-between; width: 100%; gap: 8px; background: none; cursor: pointer; font-weight: 600; border-style: solid; border-width: 1px; padding: 10px 12px;"><span>Beden</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" data-part="icon" style="transform: none; transition: transform 150ms;"><path d="M6 9l6 6 6-6"></path></svg></button></div></div>"`;
4
+
5
+ exports[`default render is frozen renders an opened panel unchanged 1`] = `"<div data-facet-filters="" data-facet-layout="panel"><button type="button" data-facet-trigger="" data-part="trigger" aria-haspopup="dialog" aria-expanded="true" style="display: inline-flex; align-items: center; gap: var(--facet-filters-trigger-gap, 8px); background: none; padding: 0px; cursor: pointer;" data-state="open"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true" data-part="trigger-icon"><path d="M3 5h18l-7 8v6l-4 2v-8L3 5z" stroke-linejoin="round"></path></svg><span data-facet-trigger-label="">Filters</span></button><div data-facet-panel-root="" style="position: fixed; inset: 0; z-index: 1000; display: flex; padding: 0px;"><div data-facet-backdrop="" data-part="backdrop" data-state="open" style="position: absolute; inset: 0; background: rgba(0, 0, 0, 0.5);"></div><div data-facet-panel="" data-facet-panel-mode="drawer" data-part="panel" data-state="open" role="dialog" aria-modal="true" aria-label="Filters" tabindex="-1" style="display: flex; flex-direction: column; text-align: left; outline: none; position: absolute; top: 0px; bottom: 0px; right: 0px; box-shadow: 0 0 24px rgba(0, 0, 0, 0.2);"><div data-facet-panel-header="" data-part="panel-header" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; flex: 0 0 auto; padding: 12px 16px;"><span data-facet-panel-title="" data-part="panel-title" style="font-weight: 600;">Filters</span><button type="button" data-facet-panel-close="" data-part="close" aria-label="Close" style="display: inline-flex; background: none; padding: 4px; cursor: pointer;"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"></path></svg></button></div><div data-facet-panel-body="" data-part="list" style="display: flex; flex-direction: column; flex: 1 1 auto; gap: var(--facet-filters-gap, 16px); padding: 16px; overflow-y: auto;"><div data-facet-group="color" data-part="group" data-state="open"><button type="button" data-facet-toggle="" aria-expanded="true" data-part="label" data-state="open" style="display: flex; align-items: center; justify-content: space-between; width: 100%; gap: 8px; background: none; cursor: pointer; font-weight: 600; padding: 8px 0px;"><span>Renk</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" data-part="icon" data-state="open" style="transform: rotate(180deg); transition: transform 150ms;"><path d="M6 9l6 6 6-6"></path></svg></button><ul data-facet-choices="" data-part="option-list" style="list-style: none; margin: 0px; padding: 0px; display: flex; flex-direction: column; flex-wrap: nowrap; gap: 6px;"><li><button type="button" data-facet-choice="black" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>Siyah</span><span data-part="count" style="opacity: 0.6;">(12)</span></button></li><li><button type="button" data-facet-choice="white" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>Beyaz</span><span data-part="count" style="opacity: 0.6;">(7)</span></button></li><li><button type="button" data-facet-choice="navy" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>Lacivert</span><span data-part="count" style="opacity: 0.6;">(3)</span></button></li></ul></div><div data-facet-group="size" data-part="group" data-state="open"><button type="button" data-facet-toggle="" aria-expanded="true" data-part="label" data-state="open" style="display: flex; align-items: center; justify-content: space-between; width: 100%; gap: 8px; background: none; cursor: pointer; font-weight: 600; padding: 8px 0px;"><span>Beden</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" data-part="icon" data-state="open" style="transform: rotate(180deg); transition: transform 150ms;"><path d="M6 9l6 6 6-6"></path></svg></button><ul data-facet-choices="" data-part="option-list" style="list-style: none; margin: 0px; padding: 0px; display: flex; flex-direction: column; flex-wrap: nowrap; gap: 6px;"><li><button type="button" data-facet-choice="s" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>S</span><span data-part="count" style="opacity: 0.6;">(9)</span></button></li><li><button type="button" data-facet-choice="m" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>M</span><span data-part="count" style="opacity: 0.6;">(4)</span></button></li></ul></div></div><div data-facet-panel-footer="" data-part="panel-footer" style="display: flex; gap: 8px; flex: 0 0 auto; padding: 12px 16px;"><button type="button" data-facet-clear="" data-part="clear" style="flex: 1 1 0px; padding: 10px 12px; background: none; border-style: solid; border-width: 1px; cursor: pointer;">Clear all</button><button type="button" data-facet-apply="" data-part="apply" style="flex: 1 1 0px; padding: 10px 12px; background: rgb(17, 24, 39); color: rgb(255, 255, 255); cursor: pointer;">Apply</button></div></div></div></div>"`;
6
+
7
+ exports[`default render is frozen renders the pills layout unchanged 1`] = `"<div data-facet-filters="" data-facet-layout="pills" data-part="list" style="display: flex; flex-direction: column; flex-wrap: wrap; gap: var(--facet-filters-gap, 16px);"><div data-facet-group="color" data-part="group" data-state="open"><ul data-facet-choices="" data-part="option-list" style="list-style: none; margin: 0px; padding: 0px; display: flex; flex-direction: row; flex-wrap: wrap; gap: var(--facet-filters-pill-gap, 8px);"><li><a data-facet-choice="black" data-facet-selected="false" data-part="pill" href="/list?attributes_color=black" style="display: inline-flex; align-items: center; gap: 6px; border-radius: var(--facet-filters-pill-radius, 999px); text-decoration: none; cursor: pointer; white-space: nowrap;"><span>Siyah</span><span data-part="count" style="opacity: 0.6;">(12)</span></a></li><li><a data-facet-choice="white" data-facet-selected="false" data-part="pill" href="/list?attributes_color=white" style="display: inline-flex; align-items: center; gap: 6px; border-radius: var(--facet-filters-pill-radius, 999px); text-decoration: none; cursor: pointer; white-space: nowrap;"><span>Beyaz</span><span data-part="count" style="opacity: 0.6;">(7)</span></a></li><li><a data-facet-choice="navy" data-facet-selected="false" data-part="pill" href="/list?attributes_color=navy" style="display: inline-flex; align-items: center; gap: 6px; border-radius: var(--facet-filters-pill-radius, 999px); text-decoration: none; cursor: pointer; white-space: nowrap;"><span>Lacivert</span><span data-part="count" style="opacity: 0.6;">(3)</span></a></li></ul></div><div data-facet-group="size" data-part="group" data-state="open"><ul data-facet-choices="" data-part="option-list" style="list-style: none; margin: 0px; padding: 0px; display: flex; flex-direction: row; flex-wrap: wrap; gap: var(--facet-filters-pill-gap, 8px);"><li><a data-facet-choice="s" data-facet-selected="false" data-part="pill" href="/list?attributes_size=s" style="display: inline-flex; align-items: center; gap: 6px; border-radius: var(--facet-filters-pill-radius, 999px); text-decoration: none; cursor: pointer; white-space: nowrap;"><span>S</span><span data-part="count" style="opacity: 0.6;">(9)</span></a></li><li><a data-facet-choice="m" data-facet-selected="false" data-part="pill" href="/list?attributes_size=m" style="display: inline-flex; align-items: center; gap: 6px; border-radius: var(--facet-filters-pill-radius, 999px); text-decoration: none; cursor: pointer; white-space: nowrap;"><span>M</span><span data-part="count" style="opacity: 0.6;">(4)</span></a></li></ul></div></div>"`;
8
+
9
+ exports[`default render is frozen renders the sidebar layout unchanged 1`] = `"<div data-facet-filters="" data-facet-layout="sidebar" data-part="list" style="display: flex; flex-direction: column; flex-wrap: nowrap; gap: var(--facet-filters-gap, 16px);"><div data-facet-group="color" data-part="group" data-state="open"><button type="button" data-facet-toggle="" aria-expanded="true" data-part="label" data-state="open" style="display: flex; align-items: center; justify-content: space-between; width: 100%; gap: 8px; background: none; cursor: pointer; font-weight: 600; padding: 8px 0px;"><span>Renk</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" data-part="icon" data-state="open" style="transform: rotate(180deg); transition: transform 150ms;"><path d="M6 9l6 6 6-6"></path></svg></button><ul data-facet-choices="" data-part="option-list" style="list-style: none; margin: 0px; padding: 0px; display: flex; flex-direction: column; flex-wrap: nowrap; gap: 6px;"><li><button type="button" data-facet-choice="black" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>Siyah</span><span data-part="count" style="opacity: 0.6;">(12)</span></button></li><li><button type="button" data-facet-choice="white" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>Beyaz</span><span data-part="count" style="opacity: 0.6;">(7)</span></button></li><li><button type="button" data-facet-choice="navy" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>Lacivert</span><span data-part="count" style="opacity: 0.6;">(3)</span></button></li></ul></div><div data-facet-group="size" data-part="group" data-state="open"><button type="button" data-facet-toggle="" aria-expanded="true" data-part="label" data-state="open" style="display: flex; align-items: center; justify-content: space-between; width: 100%; gap: 8px; background: none; cursor: pointer; font-weight: 600; padding: 8px 0px;"><span>Beden</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" data-part="icon" data-state="open" style="transform: rotate(180deg); transition: transform 150ms;"><path d="M6 9l6 6 6-6"></path></svg></button><ul data-facet-choices="" data-part="option-list" style="list-style: none; margin: 0px; padding: 0px; display: flex; flex-direction: column; flex-wrap: nowrap; gap: 6px;"><li><button type="button" data-facet-choice="s" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>S</span><span data-part="count" style="opacity: 0.6;">(9)</span></button></li><li><button type="button" data-facet-choice="m" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>M</span><span data-part="count" style="opacity: 0.6;">(4)</span></button></li></ul></div></div>"`;
10
+
11
+ exports[`default render is frozen renders the topbar layout unchanged 1`] = `"<div data-facet-filters="" data-facet-layout="topbar" data-part="list" style="display: flex; flex-direction: row; flex-wrap: wrap; gap: var(--facet-filters-gap, 16px);"><div data-facet-group="color" data-part="group" data-state="open" style="min-width: 180px;"><button type="button" data-facet-toggle="" aria-expanded="true" data-part="label" data-state="open" style="display: flex; align-items: center; justify-content: space-between; width: 100%; gap: 8px; background: none; cursor: pointer; font-weight: 600; padding: 8px 0px;"><span>Renk</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" data-part="icon" data-state="open" style="transform: rotate(180deg); transition: transform 150ms;"><path d="M6 9l6 6 6-6"></path></svg></button><ul data-facet-choices="" data-part="option-list" style="list-style: none; margin: 0px; padding: 0px; display: flex; flex-direction: column; flex-wrap: nowrap; gap: 6px;"><li><button type="button" data-facet-choice="black" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>Siyah</span><span data-part="count" style="opacity: 0.6;">(12)</span></button></li><li><button type="button" data-facet-choice="white" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>Beyaz</span><span data-part="count" style="opacity: 0.6;">(7)</span></button></li><li><button type="button" data-facet-choice="navy" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>Lacivert</span><span data-part="count" style="opacity: 0.6;">(3)</span></button></li></ul></div><div data-facet-group="size" data-part="group" data-state="open" style="min-width: 180px;"><button type="button" data-facet-toggle="" aria-expanded="true" data-part="label" data-state="open" style="display: flex; align-items: center; justify-content: space-between; width: 100%; gap: 8px; background: none; cursor: pointer; font-weight: 600; padding: 8px 0px;"><span>Beden</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" data-part="icon" data-state="open" style="transform: rotate(180deg); transition: transform 150ms;"><path d="M6 9l6 6 6-6"></path></svg></button><ul data-facet-choices="" data-part="option-list" style="list-style: none; margin: 0px; padding: 0px; display: flex; flex-direction: column; flex-wrap: nowrap; gap: 6px;"><li><button type="button" data-facet-choice="s" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>S</span><span data-part="count" style="opacity: 0.6;">(9)</span></button></li><li><button type="button" data-facet-choice="m" data-facet-selected="false" aria-pressed="false" data-part="option" style="display: flex; align-items: center; gap: 8px; width: 100%; background: none; padding: 2px 0px; cursor: pointer; text-align: left; font-weight: 400;"><span aria-hidden="true" data-part="checkbox" style="width: 14px; height: 14px; flex: 0 0 auto; border: 1px solid currentcolor; border-radius: 2px; background: transparent;"></span><span>M</span><span data-part="count" style="opacity: 0.6;">(4)</span></button></li></ul></div></div>"`;
@@ -0,0 +1,181 @@
1
+ import { generateThemeCSS } from '../utils';
2
+ import { readCustomCss, scopeCustomCss } from '../utils/custom-css';
3
+ import type { Section } from '../theme-section';
4
+
5
+ const SCOPE = '[data-block-id="blk-1"]';
6
+
7
+ const sectionWithCustomCss = (customCss: unknown): Section[] =>
8
+ [
9
+ {
10
+ id: 'sec-1',
11
+ type: 'custom',
12
+ name: 'Section',
13
+ label: 'Section',
14
+ order: 0,
15
+ hidden: false,
16
+ properties: {},
17
+ styles: {},
18
+ blocks: [
19
+ {
20
+ id: 'blk-1',
21
+ type: 'variant-selector',
22
+ label: 'Variant selector',
23
+ order: 0,
24
+ hidden: false,
25
+ value: '',
26
+ properties: { customCss },
27
+ styles: {},
28
+ blocks: []
29
+ }
30
+ ]
31
+ }
32
+ ] as unknown as Section[];
33
+
34
+ describe('scoped custom CSS', () => {
35
+ it('wraps bare declarations in the block scope', () => {
36
+ expect(scopeCustomCss('color: red;', SCOPE)).toContain(
37
+ `${SCOPE} {\ncolor: red;\n}`
38
+ );
39
+ });
40
+
41
+ it('substitutes & with the block', () => {
42
+ const css = scopeCustomCss(
43
+ '& [data-part="dropdown"] { border-radius: 0; }',
44
+ SCOPE
45
+ );
46
+
47
+ expect(css).toContain(`${SCOPE} [data-part="dropdown"] {`);
48
+ });
49
+
50
+ it('scopes a selector that does not mention the block', () => {
51
+ // Otherwise a stray `select { … }` would restyle every select on the page.
52
+ expect(scopeCustomCss('select { border: 0; }', SCOPE)).toContain(
53
+ `${SCOPE} select {`
54
+ );
55
+ });
56
+
57
+ it('scopes every selector in a list', () => {
58
+ const css = scopeCustomCss('a, button { color: blue; }', SCOPE);
59
+
60
+ expect(css).toContain(`${SCOPE} a`);
61
+ expect(css).toContain(`${SCOPE} button`);
62
+ });
63
+
64
+ it('scopes inside a media query and keeps the condition', () => {
65
+ const css = scopeCustomCss(
66
+ '@media (max-width: 768px) { & [data-part="option"] { flex: 1 1 100%; } }',
67
+ SCOPE
68
+ );
69
+
70
+ expect(css).toContain('@media (max-width: 768px) {');
71
+ expect(css).toContain(`${SCOPE} [data-part="option"] {`);
72
+ });
73
+
74
+ it('refuses CSS that closes the block early', () => {
75
+ // The classic break-out: finish my rule, then style the whole page.
76
+ expect(scopeCustomCss('color: red } body { display: none', SCOPE)).toBe('');
77
+ });
78
+
79
+ it('refuses unbalanced braces', () => {
80
+ expect(scopeCustomCss('& { color: red;', SCOPE)).toBe('');
81
+ });
82
+
83
+ it('refuses imports and script-ish values', () => {
84
+ expect(scopeCustomCss('@import url(evil.css);', SCOPE)).toBe('');
85
+ expect(
86
+ scopeCustomCss('& { background: url(javascript:alert(1)); }', SCOPE)
87
+ ).toBe('');
88
+ expect(scopeCustomCss('& { behavior: url(x.htc); }', SCOPE)).toBe('');
89
+ });
90
+
91
+ it('refuses a style tag breakout', () => {
92
+ expect(scopeCustomCss('</style><script>alert(1)</script>', SCOPE)).toBe('');
93
+ });
94
+
95
+ it('ignores comments used to smuggle a brace', () => {
96
+ expect(scopeCustomCss('/* } */ color: red;', SCOPE)).toContain('color: red');
97
+ });
98
+
99
+ it('reads the value per breakpoint', () => {
100
+ const properties = {
101
+ customCss: { desktop: 'color: red;', mobile: 'color: blue;' }
102
+ };
103
+
104
+ expect(readCustomCss(properties)).toBe('color: red;');
105
+ expect(readCustomCss(properties, 'mobile')).toBe('color: blue;');
106
+ expect(readCustomCss({}, 'mobile')).toBe('');
107
+ });
108
+
109
+ it('reaches the generated stylesheet', () => {
110
+ const css = generateThemeCSS(
111
+ sectionWithCustomCss({
112
+ desktop: '& [data-part="dropdown"] { border-radius: 0; }'
113
+ })
114
+ );
115
+
116
+ expect(css).toContain(`${SCOPE} [data-part="dropdown"]`);
117
+ expect(css).toContain('border-radius: 0');
118
+ });
119
+
120
+ it('drops unsafe custom CSS instead of emitting it', () => {
121
+ const css = generateThemeCSS(
122
+ sectionWithCustomCss({ desktop: 'color: red } body { display: none' })
123
+ );
124
+
125
+ expect(css).not.toContain('display: none');
126
+ });
127
+ });
128
+
129
+ describe('custom CSS across breakpoints', () => {
130
+ it('keeps the mobile bucket on the storefront', () => {
131
+ // The storefront has no target breakpoint — one stylesheet serves both —
132
+ // so reading a single bucket silently dropped everything written for
133
+ // mobile.
134
+ const css = generateThemeCSS(
135
+ sectionWithCustomCss({
136
+ desktop: '& { gap: 24px; }',
137
+ mobile: '& { gap: 8px; }'
138
+ })
139
+ );
140
+
141
+ expect(css).toContain('gap: 24px');
142
+ expect(css).toContain('@media (max-width: 1023px)');
143
+ expect(css).toContain('gap: 8px');
144
+ });
145
+
146
+ it('emits only the edited breakpoint in the designer', () => {
147
+ const css = generateThemeCSS(
148
+ sectionWithCustomCss({
149
+ desktop: '& { gap: 24px; }',
150
+ mobile: '& { gap: 8px; }'
151
+ }),
152
+ 'mobile'
153
+ );
154
+
155
+ expect(css).toContain('gap: 8px');
156
+ expect(css).not.toContain('gap: 24px');
157
+ });
158
+
159
+ it('does not repeat an identical mobile value', () => {
160
+ const css = generateThemeCSS(
161
+ sectionWithCustomCss({ desktop: '& { gap: 24px; }', mobile: '& { gap: 24px; }' })
162
+ );
163
+
164
+ expect(css).not.toContain('@media');
165
+ });
166
+
167
+ it('lets an animation through unscoped', () => {
168
+ // Scoping a keyframe stop turns it into a dead at-rule, so a spinner
169
+ // could not be authored at all.
170
+ const css = generateThemeCSS(
171
+ sectionWithCustomCss({
172
+ desktop:
173
+ '@keyframes pz-spin { to { transform: rotate(360deg); } } & { animation: pz-spin 1s linear infinite; }'
174
+ })
175
+ );
176
+
177
+ expect(css).toContain('@keyframes pz-spin');
178
+ expect(css).toContain('rotate(360deg)');
179
+ expect(css).toContain('animation: pz-spin');
180
+ });
181
+ });