@shopware/cms-base-layer 2.1.0 → 3.0.1

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 (109) hide show
  1. package/README.md +93 -135
  2. package/app/app.config.ts +1 -0
  3. package/app/components/SwCategoryNavigation.vue +1 -0
  4. package/app/components/SwCategoryNavigationLink.vue +16 -8
  5. package/app/components/SwContactForm.vue +1 -0
  6. package/app/components/SwFilterChips.vue +1 -0
  7. package/app/components/SwListingProductPrice.vue +16 -4
  8. package/app/components/SwMedia3D.vue +13 -9
  9. package/app/components/SwNewsletterForm.vue +4 -3
  10. package/app/components/SwProductAddToCart.vue +24 -7
  11. package/app/components/SwProductCard.vue +5 -0
  12. package/app/components/SwProductCardDetails.vue +27 -9
  13. package/app/components/SwProductCardImage.vue +52 -35
  14. package/app/components/SwProductCardSkeleton.vue +37 -14
  15. package/app/components/SwProductGallery.vue +1 -0
  16. package/app/components/SwProductListingFilter.vue +5 -1
  17. package/app/components/SwProductListingFilters.vue +23 -8
  18. package/app/components/SwProductListingFiltersHorizontal.vue +6 -3
  19. package/app/components/SwProductPrice.vue +1 -0
  20. package/app/components/SwProductReviews.vue +14 -6
  21. package/app/components/SwProductReviewsForm.vue +15 -3
  22. package/app/components/SwProductUnits.vue +1 -0
  23. package/app/components/SwQuantitySelect.vue +10 -3
  24. package/app/components/SwSharedPrice.vue +1 -0
  25. package/app/components/SwSlider.vue +1 -0
  26. package/app/components/SwStockInfo.vue +12 -6
  27. package/app/components/SwVariantConfigurator.vue +5 -3
  28. package/app/components/listing-filters/SwFilterPrice.vue +79 -30
  29. package/app/components/listing-filters/SwFilterProperties.vue +40 -25
  30. package/app/components/listing-filters/SwFilterRating.vue +21 -9
  31. package/app/components/listing-filters/SwFilterShippingFree.vue +16 -8
  32. package/app/components/public/cms/CmsBlockSpatialViewer.vue +1 -0
  33. package/app/components/public/cms/CmsGenericBlock.vue +11 -4
  34. package/app/components/public/cms/CmsGenericElement.vue +8 -2
  35. package/app/components/public/cms/CmsNoComponent.vue +88 -8
  36. package/app/components/public/cms/CmsPage.md +1 -1
  37. package/app/components/public/cms/CmsPage.vue +5 -6
  38. package/app/components/public/cms/FrontendAccountCustomerGroupRegistrationPage.vue +52 -0
  39. package/app/components/public/cms/block/CmsBlockCategoryNavigation.vue +1 -0
  40. package/app/components/public/cms/block/CmsBlockCenterText.vue +1 -0
  41. package/app/components/public/cms/block/CmsBlockCrossSelling.vue +1 -0
  42. package/app/components/public/cms/block/CmsBlockCustomForm.vue +1 -0
  43. package/app/components/public/cms/block/CmsBlockForm.vue +1 -0
  44. package/app/components/public/cms/block/CmsBlockGalleryBuybox.vue +4 -1
  45. package/app/components/public/cms/block/CmsBlockImage.vue +1 -0
  46. package/app/components/public/cms/block/CmsBlockImageBubbleRow.vue +4 -1
  47. package/app/components/public/cms/block/CmsBlockImageCover.vue +1 -0
  48. package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +4 -1
  49. package/app/components/public/cms/block/CmsBlockImageGallery.vue +1 -0
  50. package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +1 -0
  51. package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +4 -1
  52. package/app/components/public/cms/block/CmsBlockImageSimpleGrid.vue +1 -0
  53. package/app/components/public/cms/block/CmsBlockImageSlider.vue +1 -0
  54. package/app/components/public/cms/block/CmsBlockImageText.vue +4 -1
  55. package/app/components/public/cms/block/CmsBlockImageTextBubble.vue +4 -1
  56. package/app/components/public/cms/block/CmsBlockImageTextCover.vue +4 -1
  57. package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +4 -12
  58. package/app/components/public/cms/block/CmsBlockImageTextRow.vue +4 -1
  59. package/app/components/public/cms/block/CmsBlockImageThreeColumn.vue +1 -0
  60. package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +4 -1
  61. package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +4 -1
  62. package/app/components/public/cms/block/CmsBlockProductDescriptionReviews.vue +1 -0
  63. package/app/components/public/cms/block/CmsBlockProductHeading.vue +1 -0
  64. package/app/components/public/cms/block/CmsBlockProductListing.vue +1 -0
  65. package/app/components/public/cms/block/CmsBlockProductThreeColumn.vue +1 -0
  66. package/app/components/public/cms/block/CmsBlockSidebarFilter.vue +1 -0
  67. package/app/components/public/cms/block/CmsBlockText.vue +1 -0
  68. package/app/components/public/cms/block/CmsBlockTextHero.vue +1 -0
  69. package/app/components/public/cms/block/CmsBlockTextOnImage.vue +2 -4
  70. package/app/components/public/cms/block/CmsBlockTextThreeColumn.vue +1 -0
  71. package/app/components/public/cms/block/CmsBlockTextTwoColumn.vue +4 -1
  72. package/app/components/public/cms/block/CmsBlockVimeoVideo.vue +1 -0
  73. package/app/components/public/cms/block/CmsBlockYoutubeVideo.vue +1 -0
  74. package/app/components/public/cms/element/CmsElementBuyBox.vue +90 -72
  75. package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +14 -3
  76. package/app/components/public/cms/element/CmsElementCrossSelling.vue +3 -1
  77. package/app/components/public/cms/element/CmsElementCustomForm.vue +1 -0
  78. package/app/components/public/cms/element/CmsElementForm.vue +1 -0
  79. package/app/components/public/cms/element/CmsElementHtml.vue +0 -1
  80. package/app/components/public/cms/element/CmsElementImage.vue +15 -34
  81. package/app/components/public/cms/element/CmsElementImageGallery.vue +6 -2
  82. package/app/components/public/cms/element/CmsElementManufacturerLogo.vue +1 -0
  83. package/app/components/public/cms/element/CmsElementProductBox.vue +1 -0
  84. package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +77 -29
  85. package/app/components/public/cms/element/CmsElementProductListing.vue +22 -7
  86. package/app/components/public/cms/element/CmsElementProductSlider.vue +6 -4
  87. package/app/components/public/cms/element/CmsElementText.vue +2 -0
  88. package/app/components/public/cms/element/CmsElementVimeoVideo.vue +1 -0
  89. package/app/components/public/cms/element/CmsElementYoutubeVideo.vue +1 -0
  90. package/app/components/public/cms/element/SwProductListingPagination.vue +8 -2
  91. package/app/components/public/cms/section/CmsSectionSidebar.vue +13 -8
  92. package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +16 -5
  93. package/app/components/ui/SwitchButton.vue +53 -14
  94. package/app/components/ui/WishlistIcon.vue +8 -2
  95. package/app/composables/useImagePlaceholder.ts +2 -2
  96. package/app/composables/useLcpImagePreload.test.ts +1 -0
  97. package/app/composables/useLcpImagePreload.ts +12 -6
  98. package/app/composables/useTypedAppConfig.ts +16 -0
  99. package/app/helpers/cms/getImageSizes.test.ts +1 -0
  100. package/app/helpers/html-to-vue/ast.ts +1 -0
  101. package/app/helpers/html-to-vue/getOptionsFromNode.test.ts +1 -0
  102. package/app/helpers/html-to-vue/renderToHtml.ts +1 -0
  103. package/app/helpers/html-to-vue/renderer.ts +1 -0
  104. package/app/providers/shopware.test.ts +1 -0
  105. package/index.d.ts +37 -29
  106. package/nuxt.config.ts +5 -0
  107. package/package.json +22 -25
  108. package/app/plugins/unocss-runtime.client.ts +0 -23
  109. package/uno.config.ts +0 -94
package/README.md CHANGED
@@ -5,14 +5,14 @@
5
5
  [![](https://img.shields.io/github/issues/shopware/frontends/cms-base?label=cms-base%20issues&logo=github)](https://github.com/shopware/frontends/issues?q=is%3Aopen+is%3Aissue+label%3Acms-base)
6
6
  [![](https://img.shields.io/github/license/shopware/frontends?color=blue)](#)
7
7
 
8
- Nuxt [layer](https://nuxt.com/docs/getting-started/layers) that provides an implementation of all CMS components in Shopware [based on utility-classes](https://frontends.shopware.com/framework/styling.html) using atomic css syntax (UnoCss / Tailwind).
8
+ Nuxt [layer](https://nuxt.com/docs/getting-started/layers) that provides an implementation of all CMS components in Shopware [based on utility-classes](https://frontends.shopware.com/framework/styling.html).
9
9
 
10
- It is useful for projects that want to use the CMS components but design their own layout.
10
+ It is useful for projects that want to use the CMS components while keeping CMS functionality separate from the styling system and design tokens.
11
11
 
12
12
  ## Features
13
13
 
14
14
  - Vue components for [Shopping Experiences](https://www.shopware.com/en/products/shopping-experiences/) CMS
15
- - CMS sections, blocks and elements styled using [Tailwind CSS](https://tailwindcss.com/) classes
15
+ - CMS sections, blocks and elements implemented with utility-class-based markup
16
16
  - 🚀 Empowered by [@shopware/composables](https://www.npmjs.com/package/@shopware/composables)
17
17
 
18
18
  ## Setup
@@ -32,25 +32,30 @@ npm install -D @shopware/cms-base-layer
32
32
  yarn add -D @shopware/cms-base-layer
33
33
 
34
34
  # pnpm
35
- pnpm install -D @shopware/cms-base-layer
35
+ pnpm add -D @shopware/cms-base-layer
36
36
 
37
37
  # bun
38
38
  bun install -D @shopware/cms-base-layer
39
39
 
40
40
  # deno
41
- deno install --dev @shopware/cms-base-layer
41
+ deno install --dev npm:@shopware/cms-base-layer
42
42
  ```
43
43
 
44
44
  <!-- /automd -->
45
45
 
46
+ If you also want the shared Shopware Frontends UnoCSS setup, install `@shopware/unocss-design-tokens-layer` in your app and extend it alongside `@shopware/cms-base-layer`.
47
+
46
48
  Then, register the Nuxt layer in `nuxt.config.ts` file:
47
49
 
48
50
  <!-- automd:file src="templates/vue-blank/nuxt.config.ts" code -->
49
51
 
50
52
  ```ts [nuxt.config.ts]
51
53
  // https://v3.nuxtjs.org/api/configuration/nuxt.config
54
+ const isStackBlitz = process.env.SHOPWARE_STACKBLITZ === "true";
55
+
52
56
  export default defineNuxtConfig({
53
57
  extends: ["@shopware/composables/nuxt-layer", "@shopware/cms-base-layer"],
58
+ ...(isStackBlitz ? { devtools: { enabled: false } } : {}),
54
59
  shopware: {
55
60
  endpoint: "https://demo-frontends.shopware.store/store-api/",
56
61
  accessToken: "SWSCBHFSNTVMAWNZDNFKSHLAYW",
@@ -83,15 +88,20 @@ Since all CMS components are registered in your Nuxt application, you can now st
83
88
  </template>
84
89
  ```
85
90
 
86
- > You can use default styling by installing/importing Tailwind CSS stylesheet in your project.
91
+ > `@shopware/cms-base-layer` no longer owns the default UnoCSS theme. If you want the shared Shopware Frontends design tokens and UnoCSS defaults, extend `@shopware/unocss-design-tokens-layer` as shown above.
92
+
93
+ See a [short guide](https://frontends.shopware.com/getting-started/cms/content-pages.html#use-the-cms-base-package) on how to use `cms-base-layer` in your Nuxt project.
94
+
95
+ ## Styling and Design Tokens
87
96
 
88
- See a [short guide](https://frontends.shopware.com/getting-started/cms/content-pages.html#use-the-cms-base-package) how to use `cms-base` package in your project based on Nuxt v3.
97
+ The components use utility classes, but the shared UnoCSS configuration, design tokens, and runtime handling for dynamic CMS classes are now provided by `@shopware/unocss-design-tokens-layer`.
89
98
 
90
- ## Default styling
99
+ This means you have two options:
91
100
 
92
- The components are styled using [Tailwind CSS](https://tailwindcss.com/) utility classes, so you can use them in your project without any additional configuration if your project uses Tailwind CSS.
101
+ - extend `@shopware/unocss-design-tokens-layer` to use the shared Shopware Frontends token palette and UnoCSS defaults
102
+ - keep only `@shopware/cms-base-layer` and provide your own UnoCSS or Tailwind setup
93
103
 
94
- This layer provides a default Tailwind CSS configuration (see [uno.config.ts](./uno.config.ts) for details), which is used to style the components. If you want to customize the styling, you can do so by creating your own Tailwind CSS configuration file and extending the default one:
104
+ When you use the design-tokens layer, you can customize the generated config in your project's `uno.config.ts`:
95
105
 
96
106
  ```ts [nuxt.config.ts]
97
107
  // nuxt.config.ts
@@ -100,32 +110,24 @@ export default defineNuxtConfig({
100
110
  unocss: {
101
111
  nuxtLayers: true, // enable Nuxt layers for UnoCSS
102
112
  },
103
- })
104
- ```
105
-
106
- ```ts [uno.config.ts]
107
- // uno.config.ts
108
- import config from './.nuxt/uno.config.mjs'
109
-
110
- export default config
113
+ });
111
114
  ```
112
115
 
113
- Thanks to this, you can **use the default configuration** provided by this layer, or **extend/overwrite** it with your own customizations in your end-project:
114
-
115
116
  ```ts [uno.config.ts]
116
- // uno.config.ts
117
- import { mergeConfigs } from '@unocss/core'
118
- import config from './.nuxt/uno.config.mjs'
119
-
120
- export default mergeConfigs([config, {
121
- theme: {
122
- colors: {
123
- primary: '#ff3e00',
124
- secondary: '#1c1c1c',
117
+ import { mergeConfigs } from "@unocss/core";
118
+ import baseConfig from "./.nuxt/uno.config.mjs";
119
+
120
+ export default mergeConfigs([
121
+ baseConfig,
122
+ {
123
+ theme: {
124
+ colors: {
125
+ "brand-primary": "#ff3e00",
126
+ "brand-secondary": "#1c1c1c",
127
+ },
125
128
  },
126
129
  },
127
- }])
128
-
130
+ ]);
129
131
  ```
130
132
 
131
133
  See the [UnoCSS reference](https://unocss.dev/integrations/nuxt#configuration) for more information on how to configure UnoCSS in Nuxt when work with layers.
@@ -150,12 +152,14 @@ This layer includes [Nuxt Image](https://image.nuxt.com/) configuration optimize
150
152
  The layer comes pre-configured with optimized settings. No additional setup is required! The configuration includes:
151
153
 
152
154
  **Available Presets:**
155
+
153
156
  - `productCard` - Product listing images (WebP, quality 90, cover fit)
154
157
  - `productDetail` - Product detail page images (WebP, quality 90, contain fit)
155
158
  - `thumbnail` - Small thumbnails (150x150, WebP, quality 90)
156
159
  - `hero` - Hero banners (WebP, quality 95, cover fit)
157
160
 
158
161
  **Responsive Breakpoints:**
162
+
159
163
  - `xs: 320px`, `sm: 640px`, `md: 768px`, `lg: 1024px`, `xl: 1280px`, `xxl: 1536px`
160
164
 
161
165
  ### Usage in Components
@@ -196,21 +200,23 @@ Replace standard `<img>` tags with `<NuxtImg>` to enable automatic optimization:
196
200
 
197
201
  Shopware supports the following URL parameters for image transformation:
198
202
 
199
- | Modifier | Description | Example | Support |
200
- |----------|-------------|---------|---------|
201
- | `width` | Image width in pixels | `400` | ✅ Always supported |
202
- | `height` | Image height in pixels | `600` | ✅ Always supported |
203
- | `quality` | Image quality (0-100) | `85` | ⚠️ Cloud/Plugin required* |
204
- | `format` | Output format | `webp`, `avif`, `jpg`, `png` | ⚠️ Cloud/Plugin required* |
205
- | `fit` | Resize behavior | `cover`, `contain`, `fill` | ⚠️ Cloud/Plugin required* |
203
+ | Modifier | Description | Example | Support |
204
+ | --------- | ---------------------- | ---------------------------- | -------------------------- |
205
+ | `width` | Image width in pixels | `400` | ✅ Always supported |
206
+ | `height` | Image height in pixels | `600` | ✅ Always supported |
207
+ | `quality` | Image quality (0-100) | `85` | ⚠️ Cloud/Plugin required\* |
208
+ | `format` | Output format | `webp`, `avif`, `jpg`, `png` | ⚠️ Cloud/Plugin required\* |
209
+ | `fit` | Resize behavior | `cover`, `contain`, `fill` | ⚠️ Cloud/Plugin required\* |
210
+
211
+ \*Advanced transformations (quality, format, fit) are available in:
206
212
 
207
- *Advanced transformations (quality, format, fit) are available in:
208
213
  - **Shopware Cloud (SaaS)**: Built-in support via managed infrastructure. For a complete list of supported image transformation parameters, see [Fastly Image Optimizer Query Parameters](https://www.fastly.com/documentation/reference/io/#query-parameters).
209
214
  - **Self-hosted instances**: Require thumbnail processor plugins like [FroshPlatformThumbnailProcessor](https://github.com/FriendsOfShopware/FroshPlatformThumbnailProcessor) or third-party CDN integration
210
215
 
211
216
  ### How It Works
212
217
 
213
218
  This layer includes a custom Shopware provider for Nuxt Image that maps modifiers to Shopware's query parameters:
219
+
214
220
  - `width` modifier → `?width=400`
215
221
  - `height` modifier → `?height=300`
216
222
  - `quality` modifier → `?quality=85`
@@ -249,31 +255,31 @@ export default defineNuxtConfig({
249
255
  quality: 85,
250
256
 
251
257
  // Add/change formats
252
- formats: ['avif', 'webp', 'jpg'],
258
+ formats: ["avif", "webp", "jpg"],
253
259
 
254
260
  // Override or add presets
255
261
  presets: {
256
262
  // Override existing preset
257
263
  productCard: {
258
264
  modifiers: {
259
- format: 'avif',
265
+ format: "avif",
260
266
  quality: 80,
261
- fit: 'cover',
262
- }
267
+ fit: "cover",
268
+ },
263
269
  },
264
270
  // Add custom preset
265
271
  categoryBanner: {
266
272
  modifiers: {
267
- format: 'webp',
273
+ format: "webp",
268
274
  quality: 90,
269
275
  width: 1200,
270
276
  height: 400,
271
- fit: 'cover',
272
- }
273
- }
274
- }
275
- }
276
- })
277
+ fit: "cover",
278
+ },
279
+ },
280
+ },
281
+ },
282
+ });
277
283
  ```
278
284
 
279
285
  ## 🖼️ Image Placeholder
@@ -335,6 +341,7 @@ When a CMS section or block has a `backgroundMedia` set, the components call `ge
335
341
  4. Appends `format` and `quality` from `app.config.ts` if provided
336
342
 
337
343
  **Example generated URL:**
344
+
338
345
  ```
339
346
  url("https://cdn.shopware.store/.../image.jpg?width=1000&fit=crop,smart&format=webp&quality=85")
340
347
  ```
@@ -348,6 +355,7 @@ This layer includes a `useLcpImagePreload` composable that automatically preload
348
355
  ### How it works
349
356
 
350
357
  The composable scans CMS sections in document order, checking:
358
+
351
359
  1. Section background images (`section.backgroundMedia`)
352
360
  2. Block background images (`block.backgroundMedia`)
353
361
  3. Image element media (`slot.data.media`)
@@ -372,72 +380,55 @@ The preload URL includes the optimized `format` and `quality` parameters from `a
372
380
 
373
381
  ## Responsive CMS Images
374
382
 
375
- CMS image elements (`CmsElementImage`) automatically serve appropriately-sized images using responsive `srcset` and `sizes` attributes. This prevents the browser from downloading images larger than their displayed dimensions — a common Lighthouse performance issue.
383
+ Images are optimized to prevent the browser from downloading images larger than their displayed dimensions — a common Lighthouse performance issue.
376
384
 
377
- ### How it works
378
-
379
- 1. **`CmsGenericBlock`** counts the number of slots in each block, `provide`s a responsive `sizes` value (e.g., a 2-slot block means images are ~50% viewport width on desktop), and `provide`s the slot count via `cms-block-slot-count` for slider SSR breakpoint scaling.
380
- 2. **`CmsElementImage`** `inject`s the sizes hint and applies it to `<NuxtImg>`.
381
- 3. If the media has **pre-generated thumbnails** from Shopware, the existing `srcset` from thumbnails is used.
382
- 4. If **no thumbnails** exist, a synthetic `srcset` is generated using CDN width-based resizing (`?width=400`, `?width=800`, etc.) via the `generateCdnSrcSet` helper from `@shopware/helpers`.
383
- 5. **Slider components** (`CmsElementProductSlider`, `CmsElementCrossSelling`) `inject` the slot count to scale their SSR breakpoints — ensuring media queries account for the container being a fraction of the viewport (e.g., a 2-slot block doubles the breakpoint thresholds).
385
+ ### Product Card Images (`SwProductCardImage`)
384
386
 
385
- The browser combines `sizes` + `srcset` to download only the image size it actually needsduring HTML parsing, before any JavaScript runs.
386
-
387
- ### Configuration
388
-
389
- Default slot-count-to-sizes mappings are set in `app.config.ts` and can be overridden:
387
+ The `productCard` preset only defines URL modifiers (format/quality/fit). `width`/`height`/`densities`/`loading` stay on the componentNuxtImg presets don't propagate these reliably:
390
388
 
391
389
  ```ts
392
- export default defineAppConfig({
393
- imageSizes: {
394
- // slot count sizes attribute value
395
- 1: "(max-width: 768px) 100vw, 100vw", // full-width blocks
396
- 2: "(max-width: 768px) 100vw, 50vw", // two-column blocks (e.g., image-text)
397
- 3: "(max-width: 768px) 100vw, 33vw", // three-column blocks
398
- default: "(max-width: 768px) 50vw, 25vw", // 4+ columns
399
- },
400
- });
390
+ // nuxt.config.ts
391
+ productCard: {
392
+ modifiers: { format: "webp", quality: 90, fit: "cover" },
393
+ }
401
394
  ```
402
395
 
403
- For example, to cap image sizes at a fixed pixel width for boxed layouts:
404
-
405
- ```ts
406
- export default defineAppConfig({
407
- imageSizes: {
408
- 1: "(max-width: 768px) 100vw, 1200px",
409
- 2: "(max-width: 768px) 100vw, 600px",
410
- 3: "(max-width: 768px) 100vw, 400px",
411
- default: "(max-width: 768px) 50vw, 300px",
412
- },
413
- });
396
+ ```vue
397
+ <NuxtImg
398
+ preset="productCard"
399
+ :src="coverSrcPath"
400
+ width="400"
401
+ height="400"
402
+ densities="1x"
403
+ loading="lazy"
404
+ />
414
405
  ```
415
406
 
416
- ### Per-block override
407
+ - **Fixed `width`/`height`** (400px) — avoid hydration mismatches caused by dynamic DOM measurement
408
+ - **`densities="1x"`** — prevents duplicate retina requests
409
+ - **`loading="lazy"`** — defers off-viewport images
417
410
 
418
- Individual block components can override the sizes value by calling `provide("cms-image-sizes", "custom value")` this takes precedence over the default from `CmsGenericBlock`.
411
+ > **⚠️ Avoid** adding `decoding` or `sizes` props on the component they trigger Vue hydration attribute mismatches with NuxtImg, which cause duplicate image requests.
419
412
 
420
- ### Synthetic srcset fallback
413
+ ### CMS Images (`CmsElementImage`)
421
414
 
422
- When Shopware media has no thumbnails (common in Cloud/SaaS setups using CDN-based resizing), the layer generates a synthetic `srcset` using CDN query parameters:
415
+ CMS image elements use `useElementSize()` to measure the rendered container and pass the size to `<NuxtImg>` via `width`/`height` props:
423
416
 
424
- ```html
425
- <img srcset="
426
- ...image.jpg?width=400&fit=crop,smart&format=webp&quality=90 400w,
427
- ...image.jpg?width=800&fit=crop,smart&format=webp&quality=90 800w,
428
- ...image.jpg?width=1200&fit=crop,smart&format=webp&quality=90 1200w,
429
- ...image.jpg?width=1600&fit=crop,smart&format=webp&quality=90 1600w"
430
- sizes="(max-width: 768px) 100vw, 50vw"
431
- >
432
- ```
417
+ - During SSR, no image is fetched (size is `undefined`)
418
+ - After hydration, the container is measured and a single correctly-sized image is requested
419
+ - The size is multiplied by 2 (for retina) and rounded up to the nearest 100px
420
+
421
+ ### Slider Components
433
422
 
434
- The `format` and `quality` values are taken from the `backgroundImage` config in `app.config.ts`.
423
+ **Slider components** (`CmsElementProductSlider`, `CmsElementCrossSelling`) `inject` the slot count via `cms-block-slot-count` to scale their SSR breakpoints — ensuring media queries account for the container being a fraction of the viewport.
435
424
 
436
- > **Note:** Synthetic srcset requires CDN-based image resizing support. See the [Image Optimization](#%EF%B8%8F-image-optimization) section for requirements.
425
+ ### LCP Image Preloading
426
+
427
+ **`useLcpImagePreload`** scans CMS sections for the first image and injects `<link rel="preload" as="image" fetchpriority="high">` during SSR.
437
428
 
438
429
  ## 🔄 UnoCSS Runtime
439
430
 
440
- This layer includes a client-side [UnoCSS runtime](https://unocss.dev/integrations/runtime) plugin that resolves utility classes dynamically at runtime using a DOM MutationObserver. This is useful when CMS content from Shopware contains utility classes that aren't known at build time (e.g., inline styles or dynamic class bindings from the admin panel).
431
+ When you extend `@shopware/unocss-design-tokens-layer`, you also get a client-side [UnoCSS runtime](https://unocss.dev/integrations/runtime) plugin that resolves utility classes dynamically at runtime using a DOM MutationObserver. This is useful when CMS content from Shopware contains utility classes that aren't known at build time (for example inline utility classes configured in the admin panel).
441
432
 
442
433
  The runtime is **enabled by default**. To disable it, set `unocssRuntime` to `false` in your project's `app.config.ts`:
443
434
 
@@ -494,42 +485,9 @@ No additional packages needed to be installed.
494
485
 
495
486
  Full changelog for stable version is available [here](https://github.com/shopware/frontends/blob/main/packages/cms-base-layer/CHANGELOG.md)
496
487
 
497
- ### Latest changes: 2.1.0
498
-
499
- ### Minor Changes
500
-
501
- - [#2275](https://github.com/shopware/frontends/pull/2275) [`432dd24`](https://github.com/shopware/frontends/commit/432dd246571dfa8c149293da97d5bb16f505e54c) Thanks [@mkucmus](https://github.com/mkucmus)! - - Add configurable UnoCSS runtime plugin for dynamic CMS class support
502
- - Extend theme with overlay and fixed color tokens
503
-
504
- - [#2223](https://github.com/shopware/frontends/pull/2223) [`1db8704`](https://github.com/shopware/frontends/commit/1db870413dcea13c690504ffcaee13526bc8035f) Thanks [@mkucmus](https://github.com/mkucmus)! - Add horizontal filter layout for product listings. When the sidebar filter element is placed outside a sidebar section, filters now display as horizontal dropdowns.
505
-
506
- Includes new `SwFilterDropdown` and `SwProductListingFiltersHorizontal` components, with a `displayMode` prop added to all filter components to support both layouts.
507
-
508
- - [#2287](https://github.com/shopware/frontends/pull/2287) [`c9bde38`](https://github.com/shopware/frontends/commit/c9bde38d497d5c6c2fbd97700a362eb44ce8881f) Thanks [@mkucmus](https://github.com/mkucmus)! - Add responsive image sizing (srcset/sizes) via provide/inject, LCP preload with fetchpriority, configurable imageSizes in app.config, ISR route rules, inline styles, and AppConfig type declarations
509
-
510
- - [#2241](https://github.com/shopware/frontends/pull/2241) [`9ccbaa1`](https://github.com/shopware/frontends/commit/9ccbaa1fb6cc1f790d979c3dd3745c5402b6d8d1) Thanks [@mdanilowicz](https://github.com/mdanilowicz)! - Replace `withDefaults` by props destructure
511
-
512
- - [#2273](https://github.com/shopware/frontends/pull/2273) [`18455e7`](https://github.com/shopware/frontends/commit/18455e77221fcc77b119d0ba7eae89dfce0e2941) Thanks [@mdanilowicz](https://github.com/mdanilowicz)! - Remove SwMedia3D.vue component from autoload
513
-
514
- - [#2268](https://github.com/shopware/frontends/pull/2268) [`c3fff84`](https://github.com/shopware/frontends/commit/c3fff847e46a17c9c905bd893f1c1de287426c65) Thanks [@mdanilowicz](https://github.com/mdanilowicz)! - Improve accessibility (A11y) of CMS base layer components.
515
-
516
- - [#2214](https://github.com/shopware/frontends/pull/2214) [`ccb9384`](https://github.com/shopware/frontends/commit/ccb93849be07f1b6a4e192de02579a528b5b6ac4) Thanks [@mdanilowicz](https://github.com/mdanilowicz)! - Add full TypeScript typing to html-to-vue helper functions
488
+ ### Latest changes: 3.0.1
517
489
 
518
490
  ### Patch Changes
519
491
 
520
- - [#2226](https://github.com/shopware/frontends/pull/2226) [`d77eacc`](https://github.com/shopware/frontends/commit/d77eaccdec6c56a6f2d999048c751fb9f01177d4) Thanks [@mkucmus](https://github.com/mkucmus)! - Add config prop support to `SwProductGallery` and make `CmsElementImageGallery` respect `minHeight`, `navigationArrows`, and `navigationDots` config values.
521
-
522
- - [#2275](https://github.com/shopware/frontends/pull/2275) [`432dd24`](https://github.com/shopware/frontends/commit/432dd246571dfa8c149293da97d5bb16f505e54c) Thanks [@mkucmus](https://github.com/mkucmus)! - - Fix CMS block layout: height propagation in CmsBlockImageText, conditional `h-full` in CmsElementImage, `backgroundSize` forwarding in CmsGenericBlock, `w-full` for full_width sizing mode, and exclude `sizingMode` from section inline styles
523
- - Fix vertical alignment support in CmsElementText and CmsElementProductSlider using `align-content` CSS property
524
- - Remove rounded corners from image placeholder SVG and simplify CmsBlockTextOnImage structure
525
-
526
- - [#2210](https://github.com/shopware/frontends/pull/2210) [`c6b88b7`](https://github.com/shopware/frontends/commit/c6b88b7d2c50054188356aeb0f83053554d442f5) Thanks [@mkucmus](https://github.com/mkucmus)! - Anchor tags with "btn btn-primary" classes from the API were not being
527
- transformed to Tailwind utility classes due to condition matching issues
528
- in the html-to-vue renderer.
529
-
530
- - [#2318](https://github.com/shopware/frontends/pull/2318) [`b40305f`](https://github.com/shopware/frontends/commit/b40305f9e2ec51f29c279650e411bb773438faed) Thanks [@mdanilowicz](https://github.com/mdanilowicz)! - Components (CmsElementBuyBox, SwListingProductPrice, SwProductPrice) updated to use `hasListPrice` instead of deprecated `isListPrice`.
531
-
532
- - Updated dependencies [[`9604f22`](https://github.com/shopware/frontends/commit/9604f22678150d04c3c3156fd8ee2ce440c8c8bf), [`b40305f`](https://github.com/shopware/frontends/commit/b40305f9e2ec51f29c279650e411bb773438faed), [`432dd24`](https://github.com/shopware/frontends/commit/432dd246571dfa8c149293da97d5bb16f505e54c), [`b5f7e2a`](https://github.com/shopware/frontends/commit/b5f7e2a20c9dfdde1690e9006252d847f732bc0a), [`b5f7e2a`](https://github.com/shopware/frontends/commit/b5f7e2a20c9dfdde1690e9006252d847f732bc0a), [`9604f22`](https://github.com/shopware/frontends/commit/9604f22678150d04c3c3156fd8ee2ce440c8c8bf), [`a871c7b`](https://github.com/shopware/frontends/commit/a871c7b6256b75c2e40d93fc0354ba1971420062), [`c9bde38`](https://github.com/shopware/frontends/commit/c9bde38d497d5c6c2fbd97700a362eb44ce8881f)]:
533
- - @shopware/api-client@1.5.0
534
- - @shopware/composables@1.11.0
535
- - @shopware/helpers@1.7.0
492
+ - Updated dependencies [[`8be060d`](https://github.com/shopware/frontends/commit/8be060de825ca799f98a8f045a5e7fea61f5d1a2), [`5678fb0`](https://github.com/shopware/frontends/commit/5678fb008cbd86eaddd061e004de89e6f45bb7ec), [`22611e5`](https://github.com/shopware/frontends/commit/22611e542b8f42a4f34dce5186f628f9a17f457b)]:
493
+ - @shopware/composables@1.12.0
package/app/app.config.ts CHANGED
@@ -8,6 +8,7 @@ export default defineAppConfig({
8
8
  format: "webp",
9
9
  quality: 90,
10
10
  },
11
+ lcpImagePreload: false,
11
12
  imageSizes: {
12
13
  1: "(max-width: 768px) 100vw, 100vw",
13
14
  2: "(max-width: 768px) 100vw, 50vw",
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { ref } from "vue";
3
+
3
4
  import type { Schemas } from "#shopware";
4
5
 
5
6
  const {
@@ -7,6 +7,7 @@ import {
7
7
  } from "@shopware/helpers";
8
8
  import { computed } from "vue";
9
9
  import { RouterLink } from "vue-router";
10
+
10
11
  import { useUrlResolver } from "#imports";
11
12
  import type { Schemas } from "#shopware";
12
13
 
@@ -43,7 +44,9 @@ const hasChildren = computed(() => {
43
44
  v-if="props.level === 0"
44
45
  class="self-stretch flex flex-col justify-center items-center"
45
46
  >
46
- <div class="self-stretch py-3 border-b border-outline-outline-variant inline-flex justify-start items-center gap-1">
47
+ <div
48
+ class="self-stretch py-3 border-b border-outline-outline-variant inline-flex justify-start items-center gap-1"
49
+ >
47
50
  <div class="flex-1 flex justify-start items-center gap-2.5">
48
51
  <RouterLink
49
52
  v-if="!urlIsAbsolute(url.path)"
@@ -76,17 +79,19 @@ const hasChildren = computed(() => {
76
79
  type="button"
77
80
  :aria-label="props.isExpanded ? 'Collapse' : 'Expand'"
78
81
  >
79
- <SwChevronIcon :direction="props.isExpanded ? 'up' : 'down'" :size="20" />
82
+ <SwChevronIcon
83
+ :direction="props.isExpanded ? 'up' : 'down'"
84
+ :size="20"
85
+ />
80
86
  </button>
81
87
  </div>
82
88
  </div>
83
89
 
84
90
  <!-- Level 2+ Categories (Nested with left padding) -->
85
- <div
86
- v-else
87
- class="self-stretch flex flex-col justify-center items-center"
88
- >
89
- <div class="self-stretch pl-4 py-1.5 inline-flex justify-start items-center gap-2">
91
+ <div v-else class="self-stretch flex flex-col justify-center items-center">
92
+ <div
93
+ class="self-stretch pl-4 py-1.5 inline-flex justify-start items-center gap-2"
94
+ >
90
95
  <div class="py-0.5 flex-1 flex justify-start items-center gap-2.5">
91
96
  <RouterLink
92
97
  v-if="!urlIsAbsolute(url.path)"
@@ -121,7 +126,10 @@ const hasChildren = computed(() => {
121
126
  type="button"
122
127
  :aria-label="props.isExpanded ? 'Collapse' : 'Expand'"
123
128
  >
124
- <SwChevronIcon :direction="props.isExpanded ? 'up' : 'down'" :size="20" />
129
+ <SwChevronIcon
130
+ :direction="props.isExpanded ? 'up' : 'down'"
131
+ :size="20"
132
+ />
125
133
  </button>
126
134
  </div>
127
135
  </div>
@@ -7,6 +7,7 @@ import { useVuelidate } from "@vuelidate/core";
7
7
  import { email, minLength, required } from "@vuelidate/validators";
8
8
  import { defu } from "defu";
9
9
  import { computed, reactive, ref } from "vue";
10
+
10
11
  import {
11
12
  useCmsElementConfig,
12
13
  useNavigationContext,
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from "vue";
3
+
3
4
  import type { Schemas } from "#shopware";
4
5
 
5
6
  type FilterState = {
@@ -2,6 +2,7 @@
2
2
  import { useCmsTranslations, useProductPrice } from "@shopware/composables";
3
3
  import { defu } from "defu";
4
4
  import { toRefs } from "vue";
5
+
5
6
  import type { Schemas } from "#shopware";
6
7
 
7
8
  const props = defineProps<{
@@ -53,13 +54,18 @@ const {
53
54
  </template>
54
55
  </SwSharedPrice>
55
56
  </div>
56
- <div class="text-surface-on-surface-variant text-sm font-normal leading-tight line-through">
57
+ <div
58
+ class="text-surface-on-surface-variant text-sm font-normal leading-tight line-through"
59
+ >
57
60
  <SwSharedPrice :value="price?.listPrice?.price" />
58
61
  </div>
59
62
  </div>
60
63
 
61
64
  <!-- Regular price display -->
62
- <div v-else class="text-surface-on-surface text-base font-bold leading-normal">
65
+ <div
66
+ v-else
67
+ class="text-surface-on-surface text-base font-bold leading-normal"
68
+ >
63
69
  <SwSharedPrice :value="unitPrice">
64
70
  <template #beforePrice>
65
71
  <span v-if="displayFrom || displayFromVariants" class="text-sm">{{
@@ -70,7 +76,10 @@ const {
70
76
  </div>
71
77
 
72
78
  <!-- Variants from price -->
73
- <div v-if="displayFromVariants" class="text-surface-on-surface text-base font-bold leading-normal">
79
+ <div
80
+ v-if="displayFromVariants"
81
+ class="text-surface-on-surface text-base font-bold leading-normal"
82
+ >
74
83
  <SwSharedPrice :value="displayFromVariants">
75
84
  <template #beforePrice>
76
85
  <span v-if="displayFromVariants" class="text-sm">{{
@@ -81,7 +90,10 @@ const {
81
90
  </div>
82
91
 
83
92
  <!-- Regulation price -->
84
- <div v-if="regulationPrice" class="flex gap-2 text-surface-on-surface-variant text-sm">
93
+ <div
94
+ v-if="regulationPrice"
95
+ class="flex gap-2 text-surface-on-surface-variant text-sm"
96
+ >
85
97
  {{ translations.listing.previously }}
86
98
  <SwSharedPrice :value="regulationPrice" />
87
99
  </div>
@@ -1,8 +1,8 @@
1
1
  <script lang="ts" setup>
2
- import { OrbitControls, useGLTF } from "@tresjs/cientos";
2
+ import { Bounds, OrbitControls, useGLTF } from "@tresjs/cientos";
3
3
  import { TresCanvas } from "@tresjs/core";
4
4
  import { BasicShadowMap, NoToneMapping, SRGBColorSpace } from "three";
5
- import { computed } from "vue";
5
+ import { computed, shallowRef } from "vue";
6
6
 
7
7
  const props = defineProps<{
8
8
  src: string;
@@ -20,16 +20,20 @@ const gl = {
20
20
 
21
21
  const { state } = await useGLTF(props.src);
22
22
  const model = computed(() => state.value?.scene);
23
+
24
+ const boundsRef = shallowRef();
25
+
26
+ function focusObject() {
27
+ boundsRef.value?.instance.lookAt(model.value);
28
+ }
23
29
  </script>
24
30
  <template>
25
31
  <TresCanvas v-bind="gl">
26
- <TresPerspectiveCamera
27
- :args="[75, 1, 0.1, 2000]"
28
- :position="[0, 0, 500]"
29
- :look-at="[0, 0, 0]"
30
- />
31
- <OrbitControls />
32
- <primitive v-if="model" :object="model" />
32
+ <TresPerspectiveCamera :args="[75, 1, 0.1, 2000]" :position="[0, 0, 500]" />
33
+ <OrbitControls make-default />
34
+ <Bounds ref="boundsRef" clip use-mounted>
35
+ <primitive v-if="model" :object="model" @click="focusObject" />
36
+ </Bounds>
33
37
  <TresDirectionalLight :position="[3, 3, 3]" :intensity="1" />
34
38
  <TresAmbientLight :intensity="2" />
35
39
  </TresCanvas>
@@ -8,6 +8,7 @@ import type { ValidationRuleWithoutParams } from "@vuelidate/core";
8
8
  import { email, required } from "@vuelidate/validators";
9
9
  import { defu } from "defu";
10
10
  import { computed, reactive, ref } from "vue";
11
+
11
12
  import { useCmsElementConfig, useNewsletter, useSalutations } from "#imports";
12
13
 
13
14
  const props = defineProps<{
@@ -236,7 +237,7 @@ const invokeSubmit = async () => {
236
237
  id="salutation"
237
238
  v-model="state.salutationId"
238
239
  name="salutation"
239
- class=" border-outline-outline-variant focus:border-brand-primaryappearance-none relative block w-full px-3 py-2 border placeholder-surface-on-surface-variant text-surface-on-surface rounded-md focus:outline-none focus:ring-brand-primary focus:z-10 sm:text-sm"
240
+ class="border-outline-outline-variant focus:border-brand-primaryappearance-none relative block w-full px-3 py-2 border placeholder-surface-on-surface-variant text-surface-on-surface rounded-md focus:outline-none focus:ring-brand-primary focus:z-10 sm:text-sm"
240
241
  >
241
242
  <option disabled selected value="">
242
243
  {{ translations.form.salutationPlaceholder }}
@@ -261,8 +262,8 @@ const invokeSubmit = async () => {
261
262
  class="appearance-none relative block w-full px-3 py-2 border placeholder-surface-on-surface-variant text-surface-on-surface rounded-md focus:outline-none focus:ring-brand-primary focus:z-10 sm:text-sm"
262
263
  :class="[
263
264
  $v.firstName?.$error
264
- ? 'border-red-600 focus:border-red-600'
265
- : 'border-outline-outline-variant focus:border-brand-primary',
265
+ ? 'border-red-600 focus:border-red-600'
266
+ : 'border-outline-outline-variant focus:border-brand-primary',
266
267
  ]"
267
268
  :placeholder="translations.form.firstNamePlaceholder"
268
269
  @blur="$v.firstName?.$touch()"
@@ -3,6 +3,7 @@ import { useCmsTranslations } from "@shopware/composables";
3
3
  import { getCmsTranslate } from "@shopware/helpers";
4
4
  import { defu } from "defu";
5
5
  import { computed, toRefs } from "vue";
6
+
6
7
  import {
7
8
  useAddToCart,
8
9
  useCartErrorParamsResolver,
@@ -72,16 +73,32 @@ const addToCartProxy = async () => {
72
73
 
73
74
  <template>
74
75
  <div class="w-full inline-flex flex-col justify-start items-start gap-8">
75
- <SwQuantitySelect v-model="quantity" :min="product.minPurchase" :max="product.maxPurchase"
76
- :steps="product.purchaseSteps" />
77
- <SwStockInfo :availableStock="availableStock" :minPurchase="minPurchase" :deliveryTime="deliveryTime"
78
- :restockTime="restockTime" />
76
+ <SwQuantitySelect
77
+ v-model="quantity"
78
+ :min="product.minPurchase"
79
+ :max="product.maxPurchase"
80
+ :steps="product.purchaseSteps"
81
+ />
82
+ <SwStockInfo
83
+ :availableStock="availableStock"
84
+ :minPurchase="minPurchase"
85
+ :deliveryTime="deliveryTime"
86
+ :restockTime="restockTime"
87
+ />
79
88
  <div class="self-stretch flex flex-col justify-start items-start gap-1">
80
- <SwBaseButton variant="primary" size="medium" :disabled="!product?.available" block data-testid="add-to-cart-button"
81
- @click="addToCartProxy">
89
+ <SwBaseButton
90
+ variant="primary"
91
+ size="medium"
92
+ :disabled="!product?.available"
93
+ block
94
+ data-testid="add-to-cart-button"
95
+ @click="addToCartProxy"
96
+ >
82
97
  {{ translations.product.addToCart }}
83
98
  </SwBaseButton>
84
- <div class="self-stretch text-surface-on-surface text-xs font-normal leading-none">
99
+ <div
100
+ class="self-stretch text-surface-on-surface text-xs font-normal leading-none"
101
+ >
85
102
  {{ translations.product.productNumber }}: {{ productNumber }}
86
103
  </div>
87
104
  </div>