@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.
- package/README.md +93 -135
- package/app/app.config.ts +1 -0
- package/app/components/SwCategoryNavigation.vue +1 -0
- package/app/components/SwCategoryNavigationLink.vue +16 -8
- package/app/components/SwContactForm.vue +1 -0
- package/app/components/SwFilterChips.vue +1 -0
- package/app/components/SwListingProductPrice.vue +16 -4
- package/app/components/SwMedia3D.vue +13 -9
- package/app/components/SwNewsletterForm.vue +4 -3
- package/app/components/SwProductAddToCart.vue +24 -7
- package/app/components/SwProductCard.vue +5 -0
- package/app/components/SwProductCardDetails.vue +27 -9
- package/app/components/SwProductCardImage.vue +52 -35
- package/app/components/SwProductCardSkeleton.vue +37 -14
- package/app/components/SwProductGallery.vue +1 -0
- package/app/components/SwProductListingFilter.vue +5 -1
- package/app/components/SwProductListingFilters.vue +23 -8
- package/app/components/SwProductListingFiltersHorizontal.vue +6 -3
- package/app/components/SwProductPrice.vue +1 -0
- package/app/components/SwProductReviews.vue +14 -6
- package/app/components/SwProductReviewsForm.vue +15 -3
- package/app/components/SwProductUnits.vue +1 -0
- package/app/components/SwQuantitySelect.vue +10 -3
- package/app/components/SwSharedPrice.vue +1 -0
- package/app/components/SwSlider.vue +1 -0
- package/app/components/SwStockInfo.vue +12 -6
- package/app/components/SwVariantConfigurator.vue +5 -3
- package/app/components/listing-filters/SwFilterPrice.vue +79 -30
- package/app/components/listing-filters/SwFilterProperties.vue +40 -25
- package/app/components/listing-filters/SwFilterRating.vue +21 -9
- package/app/components/listing-filters/SwFilterShippingFree.vue +16 -8
- package/app/components/public/cms/CmsBlockSpatialViewer.vue +1 -0
- package/app/components/public/cms/CmsGenericBlock.vue +11 -4
- package/app/components/public/cms/CmsGenericElement.vue +8 -2
- package/app/components/public/cms/CmsNoComponent.vue +88 -8
- package/app/components/public/cms/CmsPage.md +1 -1
- package/app/components/public/cms/CmsPage.vue +5 -6
- package/app/components/public/cms/FrontendAccountCustomerGroupRegistrationPage.vue +52 -0
- package/app/components/public/cms/block/CmsBlockCategoryNavigation.vue +1 -0
- package/app/components/public/cms/block/CmsBlockCenterText.vue +1 -0
- package/app/components/public/cms/block/CmsBlockCrossSelling.vue +1 -0
- package/app/components/public/cms/block/CmsBlockCustomForm.vue +1 -0
- package/app/components/public/cms/block/CmsBlockForm.vue +1 -0
- package/app/components/public/cms/block/CmsBlockGalleryBuybox.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImage.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageBubbleRow.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageCover.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageGallery.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageSimpleGrid.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageSlider.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageText.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageTextBubble.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageTextCover.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +4 -12
- package/app/components/public/cms/block/CmsBlockImageTextRow.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageThreeColumn.vue +1 -0
- package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +4 -1
- package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +4 -1
- package/app/components/public/cms/block/CmsBlockProductDescriptionReviews.vue +1 -0
- package/app/components/public/cms/block/CmsBlockProductHeading.vue +1 -0
- package/app/components/public/cms/block/CmsBlockProductListing.vue +1 -0
- package/app/components/public/cms/block/CmsBlockProductThreeColumn.vue +1 -0
- package/app/components/public/cms/block/CmsBlockSidebarFilter.vue +1 -0
- package/app/components/public/cms/block/CmsBlockText.vue +1 -0
- package/app/components/public/cms/block/CmsBlockTextHero.vue +1 -0
- package/app/components/public/cms/block/CmsBlockTextOnImage.vue +2 -4
- package/app/components/public/cms/block/CmsBlockTextThreeColumn.vue +1 -0
- package/app/components/public/cms/block/CmsBlockTextTwoColumn.vue +4 -1
- package/app/components/public/cms/block/CmsBlockVimeoVideo.vue +1 -0
- package/app/components/public/cms/block/CmsBlockYoutubeVideo.vue +1 -0
- package/app/components/public/cms/element/CmsElementBuyBox.vue +90 -72
- package/app/components/public/cms/element/CmsElementCategoryNavigation.vue +14 -3
- package/app/components/public/cms/element/CmsElementCrossSelling.vue +3 -1
- package/app/components/public/cms/element/CmsElementCustomForm.vue +1 -0
- package/app/components/public/cms/element/CmsElementForm.vue +1 -0
- package/app/components/public/cms/element/CmsElementHtml.vue +0 -1
- package/app/components/public/cms/element/CmsElementImage.vue +15 -34
- package/app/components/public/cms/element/CmsElementImageGallery.vue +6 -2
- package/app/components/public/cms/element/CmsElementManufacturerLogo.vue +1 -0
- package/app/components/public/cms/element/CmsElementProductBox.vue +1 -0
- package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +77 -29
- package/app/components/public/cms/element/CmsElementProductListing.vue +22 -7
- package/app/components/public/cms/element/CmsElementProductSlider.vue +6 -4
- package/app/components/public/cms/element/CmsElementText.vue +2 -0
- package/app/components/public/cms/element/CmsElementVimeoVideo.vue +1 -0
- package/app/components/public/cms/element/CmsElementYoutubeVideo.vue +1 -0
- package/app/components/public/cms/element/SwProductListingPagination.vue +8 -2
- package/app/components/public/cms/section/CmsSectionSidebar.vue +13 -8
- package/app/components/public/cms/skeleton/ProductCardSkeleton.vue +16 -5
- package/app/components/ui/SwitchButton.vue +53 -14
- package/app/components/ui/WishlistIcon.vue +8 -2
- package/app/composables/useImagePlaceholder.ts +2 -2
- package/app/composables/useLcpImagePreload.test.ts +1 -0
- package/app/composables/useLcpImagePreload.ts +12 -6
- package/app/composables/useTypedAppConfig.ts +16 -0
- package/app/helpers/cms/getImageSizes.test.ts +1 -0
- package/app/helpers/html-to-vue/ast.ts +1 -0
- package/app/helpers/html-to-vue/getOptionsFromNode.test.ts +1 -0
- package/app/helpers/html-to-vue/renderToHtml.ts +1 -0
- package/app/helpers/html-to-vue/renderer.ts +1 -0
- package/app/providers/shopware.test.ts +1 -0
- package/index.d.ts +37 -29
- package/nuxt.config.ts +5 -0
- package/package.json +22 -25
- package/app/plugins/unocss-runtime.client.ts +0 -23
- package/uno.config.ts +0 -94
package/README.md
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
[](https://github.com/shopware/frontends/issues?q=is%3Aopen+is%3Aissue+label%3Acms-base)
|
|
6
6
|
[](#)
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
>
|
|
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
|
-
|
|
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
|
-
|
|
99
|
+
This means you have two options:
|
|
91
100
|
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
import
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
200
|
-
|
|
201
|
-
| `width`
|
|
202
|
-
| `height`
|
|
203
|
-
| `quality` | Image quality (0-100)
|
|
204
|
-
| `format`
|
|
205
|
-
| `fit`
|
|
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: [
|
|
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:
|
|
265
|
+
format: "avif",
|
|
260
266
|
quality: 80,
|
|
261
|
-
fit:
|
|
262
|
-
}
|
|
267
|
+
fit: "cover",
|
|
268
|
+
},
|
|
263
269
|
},
|
|
264
270
|
// Add custom preset
|
|
265
271
|
categoryBanner: {
|
|
266
272
|
modifiers: {
|
|
267
|
-
format:
|
|
273
|
+
format: "webp",
|
|
268
274
|
quality: 90,
|
|
269
275
|
width: 1200,
|
|
270
276
|
height: 400,
|
|
271
|
-
fit:
|
|
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
|
-
|
|
383
|
+
Images are optimized to prevent the browser from downloading images larger than their displayed dimensions — a common Lighthouse performance issue.
|
|
376
384
|
|
|
377
|
-
###
|
|
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
|
|
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 component — NuxtImg presets don't propagate these reliably:
|
|
390
388
|
|
|
391
389
|
```ts
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
413
|
+
### CMS Images (`CmsElementImage`)
|
|
421
414
|
|
|
422
|
-
|
|
415
|
+
CMS image elements use `useElementSize()` to measure the rendered container and pass the size to `<NuxtImg>` via `width`/`height` props:
|
|
423
416
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
- [
|
|
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
|
@@ -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
|
|
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
|
|
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
|
-
|
|
87
|
-
|
|
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
|
|
129
|
+
<SwChevronIcon
|
|
130
|
+
:direction="props.isExpanded ? 'up' : 'down'"
|
|
131
|
+
:size="20"
|
|
132
|
+
/>
|
|
125
133
|
</button>
|
|
126
134
|
</div>
|
|
127
135
|
</div>
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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="
|
|
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
|
-
|
|
265
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
:
|
|
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
|
|
81
|
-
|
|
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
|
|
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>
|