@shopware/cms-base-layer 2.0.0 → 2.1.0
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 +168 -100
- package/app/app.config.ts +11 -0
- package/app/components/SwCategoryNavigation.vue +25 -18
- package/app/components/SwFilterDropdown.vue +54 -0
- package/app/components/SwListingProductPrice.vue +2 -2
- package/app/components/SwMedia3D.vue +4 -2
- package/app/components/SwProductCard.vue +20 -21
- package/app/components/SwProductCardDetails.vue +29 -12
- package/app/components/SwProductCardImage.vue +4 -1
- package/app/components/SwProductGallery.vue +18 -14
- package/app/components/SwProductListingFilter.vue +20 -9
- package/app/components/SwProductListingFilters.vue +1 -5
- package/app/components/SwProductListingFiltersHorizontal.vue +306 -0
- package/app/components/SwProductPrice.vue +3 -3
- package/app/components/SwProductRating.vue +40 -0
- package/app/components/SwProductReviews.vue +6 -19
- package/app/components/SwProductUnits.vue +10 -15
- package/app/components/SwQuantitySelect.vue +4 -7
- package/app/components/SwSlider.vue +150 -51
- package/app/components/SwSortDropdown.vue +10 -6
- package/app/components/SwVariantConfigurator.vue +12 -11
- package/app/components/listing-filters/SwFilterPrice.vue +45 -40
- package/app/components/listing-filters/SwFilterProperties.vue +40 -33
- package/app/components/listing-filters/SwFilterRating.vue +36 -27
- package/app/components/listing-filters/SwFilterShippingFree.vue +39 -32
- package/app/components/public/cms/CmsBlockSpatialViewer.vue +94 -0
- package/app/components/public/cms/CmsGenericBlock.md +17 -2
- package/app/components/public/cms/CmsGenericBlock.vue +15 -1
- package/app/components/public/cms/CmsPage.md +19 -2
- package/app/components/public/cms/CmsPage.vue +11 -1
- package/app/components/public/cms/block/CmsBlockCenterText.vue +1 -1
- package/app/components/public/cms/block/CmsBlockImageText.vue +5 -5
- package/app/components/public/cms/block/CmsBlockTextOnImage.vue +5 -12
- package/app/components/public/cms/element/CmsElementBuyBox.vue +3 -3
- package/app/components/public/cms/element/CmsElementCrossSelling.vue +19 -3
- package/app/components/public/cms/element/CmsElementImage.vue +34 -36
- package/app/components/public/cms/element/CmsElementImageGallery.vue +117 -50
- package/app/components/public/cms/element/CmsElementProductBox.vue +7 -1
- package/app/components/public/cms/element/CmsElementProductListing.vue +10 -3
- package/app/components/public/cms/element/CmsElementProductName.vue +6 -1
- package/app/components/public/cms/element/CmsElementProductSlider.vue +56 -35
- package/app/components/public/cms/element/CmsElementSidebarFilter.vue +10 -2
- package/app/components/public/cms/element/CmsElementText.vue +10 -11
- package/app/components/public/cms/element/SwProductListingPagination.vue +2 -2
- package/app/components/public/cms/section/CmsSectionDefault.vue +2 -2
- package/app/components/public/cms/section/CmsSectionSidebar.vue +6 -3
- package/app/components/ui/BaseButton.vue +18 -15
- package/app/components/ui/ChevronIcon.vue +10 -13
- package/app/components/ui/WishlistIcon.vue +3 -8
- package/app/composables/useImagePlaceholder.ts +1 -1
- package/app/composables/useLcpImagePreload.test.ts +229 -0
- package/app/composables/useLcpImagePreload.ts +39 -0
- package/app/helpers/cms/findFirstCmsImageUrl.ts +86 -0
- package/app/helpers/cms/getImageSizes.test.ts +50 -0
- package/app/helpers/cms/getImageSizes.ts +36 -0
- package/app/helpers/html-to-vue/ast.ts +53 -19
- package/app/helpers/html-to-vue/getOptionsFromNode.ts +1 -1
- package/app/helpers/html-to-vue/renderToHtml.ts +7 -11
- package/app/helpers/html-to-vue/renderer.ts +86 -26
- package/app/plugins/unocss-runtime.client.ts +23 -0
- package/index.d.ts +24 -0
- package/nuxt.config.ts +20 -0
- package/package.json +23 -21
- package/uno.config.ts +11 -0
package/README.md
CHANGED
|
@@ -304,6 +304,151 @@ const customPlaceholder = useImagePlaceholder("#FF0000");
|
|
|
304
304
|
</template>
|
|
305
305
|
```
|
|
306
306
|
|
|
307
|
+
## 🖼️ Background Image Optimization
|
|
308
|
+
|
|
309
|
+
CMS sections and blocks can have background images set via the Shopware admin. This layer automatically optimizes those background image URLs by appending `format` and `quality` query parameters — bringing the same optimization applied to `<NuxtImg>` components to CSS background images.
|
|
310
|
+
|
|
311
|
+
Both `CmsPage` (for section backgrounds) and `CmsGenericBlock` (for block backgrounds) read the configuration from `app.config.ts` and pass it to the `getBackgroundImageUrl` helper from `@shopware/helpers`.
|
|
312
|
+
|
|
313
|
+
### Configuration
|
|
314
|
+
|
|
315
|
+
Default values are set in `app.config.ts` and can be overridden in your project:
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
export default defineAppConfig({
|
|
319
|
+
backgroundImage: {
|
|
320
|
+
format: "webp", // Default: "webp" — output format ("webp" | "avif" | "jpg" | "png")
|
|
321
|
+
quality: 90, // Default: 90 — image quality (0-100)
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Setting `format` or `quality` to `undefined` (or omitting the key) will skip that parameter in the generated URL.
|
|
327
|
+
|
|
328
|
+
### How It Works
|
|
329
|
+
|
|
330
|
+
When a CMS section or block has a `backgroundMedia` set, the components call `getBackgroundImageUrl()` which:
|
|
331
|
+
|
|
332
|
+
1. Extracts the raw image URL from the CSS `url()` value
|
|
333
|
+
2. Appends `width` or `height` based on the image's original dimensions (capped at 1920px)
|
|
334
|
+
3. Adds `fit=crop,smart` for intelligent cropping
|
|
335
|
+
4. Appends `format` and `quality` from `app.config.ts` if provided
|
|
336
|
+
|
|
337
|
+
**Example generated URL:**
|
|
338
|
+
```
|
|
339
|
+
url("https://cdn.shopware.store/.../image.jpg?width=1000&fit=crop,smart&format=webp&quality=85")
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
> **Note:** Like other dynamic image transformations, background image optimization requires remote thumbnail generation support. See the [Image Optimization](#%EF%B8%8F-image-optimization) section above for Shopware Cloud vs. self-hosted requirements.
|
|
343
|
+
|
|
344
|
+
## LCP Image Preload
|
|
345
|
+
|
|
346
|
+
This layer includes a `useLcpImagePreload` composable that automatically preloads the first image found in CMS page content. This targets the [Largest Contentful Paint (LCP)](https://web.dev/lcp/) element, which is often a hero background image or the first visible image element.
|
|
347
|
+
|
|
348
|
+
### How it works
|
|
349
|
+
|
|
350
|
+
The composable scans CMS sections in document order, checking:
|
|
351
|
+
1. Section background images (`section.backgroundMedia`)
|
|
352
|
+
2. Block background images (`block.backgroundMedia`)
|
|
353
|
+
3. Image element media (`slot.data.media`)
|
|
354
|
+
|
|
355
|
+
The first image found is injected as a `<link rel="preload" as="image" fetchpriority="high">` in the `<head>` during SSR. This allows the browser to start fetching the LCP image immediately, before parsing CSS or executing JavaScript. The `fetchpriority="high"` attribute ensures the preload is prioritized — this is especially useful for background images which don't natively support `fetchpriority`.
|
|
356
|
+
|
|
357
|
+
### Usage
|
|
358
|
+
|
|
359
|
+
The composable is already called in `CmsPage.vue`. If you override `CmsPage`, you can use it in your custom component:
|
|
360
|
+
|
|
361
|
+
```vue
|
|
362
|
+
<script setup>
|
|
363
|
+
import { useLcpImagePreload } from "@shopware/cms-base-layer/composables/useLcpImagePreload";
|
|
364
|
+
|
|
365
|
+
const props = defineProps<{ content: Schemas["CmsPage"] }>();
|
|
366
|
+
|
|
367
|
+
useLcpImagePreload(props.content?.sections || []);
|
|
368
|
+
</script>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
The preload URL includes the optimized `format` and `quality` parameters from `app.config.ts` for both background images and element images.
|
|
372
|
+
|
|
373
|
+
## Responsive CMS Images
|
|
374
|
+
|
|
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.
|
|
376
|
+
|
|
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).
|
|
384
|
+
|
|
385
|
+
The browser combines `sizes` + `srcset` to download only the image size it actually needs — during 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:
|
|
390
|
+
|
|
391
|
+
```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
|
+
});
|
|
401
|
+
```
|
|
402
|
+
|
|
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
|
+
});
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Per-block override
|
|
417
|
+
|
|
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`.
|
|
419
|
+
|
|
420
|
+
### Synthetic srcset fallback
|
|
421
|
+
|
|
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:
|
|
423
|
+
|
|
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
|
+
```
|
|
433
|
+
|
|
434
|
+
The `format` and `quality` values are taken from the `backgroundImage` config in `app.config.ts`.
|
|
435
|
+
|
|
436
|
+
> **Note:** Synthetic srcset requires CDN-based image resizing support. See the [Image Optimization](#%EF%B8%8F-image-optimization) section for requirements.
|
|
437
|
+
|
|
438
|
+
## 🔄 UnoCSS Runtime
|
|
439
|
+
|
|
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).
|
|
441
|
+
|
|
442
|
+
The runtime is **enabled by default**. To disable it, set `unocssRuntime` to `false` in your project's `app.config.ts`:
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
export default defineAppConfig({
|
|
446
|
+
unocssRuntime: false,
|
|
447
|
+
});
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
> **When to disable**: If you don't use dynamic CMS utility classes, or if you experience performance issues caused by the MutationObserver in pages with frequent DOM mutations.
|
|
451
|
+
|
|
307
452
|
## 📘 Available components
|
|
308
453
|
|
|
309
454
|
The list of available blocks and elements is [here](https://frontends.shopware.com/packages/cms-base-layer.html#available-components).
|
|
@@ -349,119 +494,42 @@ No additional packages needed to be installed.
|
|
|
349
494
|
|
|
350
495
|
Full changelog for stable version is available [here](https://github.com/shopware/frontends/blob/main/packages/cms-base-layer/CHANGELOG.md)
|
|
351
496
|
|
|
352
|
-
### Latest changes: 2.
|
|
497
|
+
### Latest changes: 2.1.0
|
|
353
498
|
|
|
354
|
-
###
|
|
499
|
+
### Minor Changes
|
|
355
500
|
|
|
356
|
-
- [#
|
|
357
|
-
-
|
|
358
|
-
- Updates the `SwProductAddToCart` component to use the new `SwQuantitySelect` component
|
|
359
|
-
- Fixes the `Status` component to use the new state classes
|
|
360
|
-
- Updates the `uno.config.ts` file to include default styling that can be used and extended in the end-project:
|
|
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
|
|
361
503
|
|
|
362
|
-
|
|
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.
|
|
363
505
|
|
|
364
|
-
|
|
365
|
-
// nuxt.config.ts in your end-project
|
|
366
|
-
{
|
|
367
|
-
unocss: {
|
|
368
|
-
nuxtLayers: true; // enable Nuxt layers support in order to merge UnoCSS configurations
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
```
|
|
506
|
+
Includes new `SwFilterDropdown` and `SwProductListingFiltersHorizontal` components, with a `displayMode` prop added to all filter components to support both layouts.
|
|
372
507
|
|
|
373
|
-
|
|
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
|
|
374
509
|
|
|
375
|
-
|
|
376
|
-
// uno.config.ts in your end-project
|
|
377
|
-
import { mergeConfigs } from "@unocss/core";
|
|
378
|
-
import baseConfig from "./.nuxt/uno.config.mjs";
|
|
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
|
|
379
511
|
|
|
380
|
-
|
|
381
|
-
// will be merged with the base config - all optional
|
|
382
|
-
theme: {
|
|
383
|
-
colors: {
|
|
384
|
-
"brand-primary": "#ff3e00",
|
|
385
|
-
"brand-secondary": "#ff6a00",
|
|
386
|
-
},
|
|
387
|
-
},
|
|
388
|
-
safelist: ["states-success"],
|
|
389
|
-
preflights: [
|
|
390
|
-
{
|
|
391
|
-
getCSS: () => `
|
|
392
|
-
body {
|
|
393
|
-
font-family: 'Inter', sans-serif;
|
|
394
|
-
-moz-osx-font-smoothing: grayscale;
|
|
395
|
-
-webkit-font-smoothing: antialiased;
|
|
396
|
-
}
|
|
397
|
-
`,
|
|
398
|
-
},
|
|
399
|
-
],
|
|
400
|
-
});
|
|
401
|
-
```
|
|
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
|
|
402
513
|
|
|
403
|
-
|
|
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.
|
|
404
515
|
|
|
405
|
-
- [#
|
|
406
|
-
|
|
407
|
-
### Features
|
|
408
|
-
- New UI components:
|
|
409
|
-
- SwFilterChips – shows active filters as removable chips
|
|
410
|
-
- SwSortDropdown – sorting dropdown
|
|
411
|
-
- SwProductListingPagination – listing pagination
|
|
412
|
-
- Checkbox – reusable checkbox
|
|
413
|
-
- ChevronIcon – configurable chevron (up/down/left/right)
|
|
414
|
-
- RadioButton – reusable radio button
|
|
415
|
-
- SwitchButton – toggle switch
|
|
416
|
-
|
|
417
|
-
### Refactors
|
|
418
|
-
- SwFilterProperties:
|
|
419
|
-
- Replace computed factory with `isChecked()` and `selectValue()` helpers for better performance and readability.
|
|
420
|
-
- Filter collapse animation:
|
|
421
|
-
- Unified expand/collapse animations for SwFilterProperties, SwFilterRating, SwFilterShippingFree, and SwFilterPrice using UnoCSS preflights.
|
|
422
|
-
|
|
423
|
-
### TypeScript fixes
|
|
424
|
-
- SwProductListingFilters:
|
|
425
|
-
- Provide fallbacks (`?? []`, `?? ''`) when passing `getSortingOrders` and `getCurrentSortingOrder`.
|
|
426
|
-
- SwFilterChips:
|
|
427
|
-
- Relax prop types to accept union types compatible with both full Shopware schemas and simplified helper types.
|
|
428
|
-
|
|
429
|
-
### Code quality improvements
|
|
430
|
-
- SwFilterPrice:
|
|
431
|
-
- Remove unnecessary optional chaining on `props.selectedFilters` to prevent masking undefined errors
|
|
432
|
-
- Checkbox component:
|
|
433
|
-
- Replace `outline-blue-500` with `outline-brand-primary` for brand consistency
|
|
434
|
-
- Make `label` prop optional to support checkbox-only pattern
|
|
435
|
-
- SwFilterShippingFree:
|
|
436
|
-
- Add i18n support using `useCmsTranslations` instead of hardcoded "free delivery" text
|
|
437
|
-
- SwFilterProperties:
|
|
438
|
-
- Remove unnecessary empty label prop from Checkbox usage
|
|
439
|
-
|
|
440
|
-
**Note:** Transition classes are globally available via UnoCSS preflights.
|
|
441
|
-
|
|
442
|
-
- [#2139](https://github.com/shopware/frontends/pull/2139) [`20d1066`](https://github.com/shopware/frontends/commit/20d106638958170dd191ac3d95e3e536f3fcc787) Thanks [@mkucmus](https://github.com/mkucmus)! - Added a new `SwProductReviewsForm` component that allows logged-in customers to submit product reviews.
|
|
443
|
-
|
|
444
|
-
- [#1959](https://github.com/shopware/frontends/pull/1959) [`c77daa6`](https://github.com/shopware/frontends/commit/c77daa6a11e96c7f3688b16f7da010b54c7f5e8b) Thanks [@patzick](https://github.com/patzick)! - Updated default types to Shopware 6.7
|
|
445
|
-
|
|
446
|
-
- [#2176](https://github.com/shopware/frontends/pull/2176) [`c647baf`](https://github.com/shopware/frontends/commit/c647baf93e7174b849f5961ee5803add99d78602) Thanks [@mkucmus](https://github.com/mkucmus)! - - Extract product listing data early from CMS page responses to enable SSR rendering
|
|
447
|
-
- Remove ClientOnly wrappers from `SwProductListingFilters` and `SwFilterChips` components
|
|
448
|
-
- Resolve hydration mismatches on category pages with filters
|
|
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
|
|
449
517
|
|
|
450
518
|
### Patch Changes
|
|
451
519
|
|
|
452
|
-
- [#
|
|
453
|
-
|
|
454
|
-
- [#2162](https://github.com/shopware/frontends/pull/2162) [`e1fae3e`](https://github.com/shopware/frontends/commit/e1fae3eb6430e5c8e133456fbaf7f215f80c36f6) Thanks [@mkucmus](https://github.com/mkucmus)! - Replace hardcoded colors with theme tokens, add image placeholder composable, improve URL encoding for special characters in image paths, enhance CMS block layouts, and use useTemplateRef for better type safety
|
|
455
|
-
|
|
456
|
-
- [#2128](https://github.com/shopware/frontends/pull/2128) [`efe125e`](https://github.com/shopware/frontends/commit/efe125e7bea273bb904356114cf93adf68a416fb) Thanks [@mkucmus](https://github.com/mkucmus)! - Enhanced SwProductReviews component with reviewer names, shop feedback, and star ratings using direct SVG imports.
|
|
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.
|
|
457
521
|
|
|
458
|
-
- [#
|
|
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
|
|
459
525
|
|
|
460
|
-
- [#
|
|
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.
|
|
461
529
|
|
|
462
|
-
- [#
|
|
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`.
|
|
463
531
|
|
|
464
|
-
- Updated dependencies [[`
|
|
465
|
-
- @shopware/
|
|
466
|
-
- @shopware/
|
|
467
|
-
- @shopware/
|
|
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
|
package/app/app.config.ts
CHANGED
|
@@ -4,4 +4,15 @@ export default defineAppConfig({
|
|
|
4
4
|
imagePlaceholder: {
|
|
5
5
|
color: "#543B95",
|
|
6
6
|
},
|
|
7
|
+
backgroundImage: {
|
|
8
|
+
format: "webp",
|
|
9
|
+
quality: 90,
|
|
10
|
+
},
|
|
11
|
+
imageSizes: {
|
|
12
|
+
1: "(max-width: 768px) 100vw, 100vw",
|
|
13
|
+
2: "(max-width: 768px) 100vw, 50vw",
|
|
14
|
+
3: "(max-width: 768px) 100vw, 33vw",
|
|
15
|
+
default: "(max-width: 768px) 50vw, 25vw",
|
|
16
|
+
},
|
|
17
|
+
unocssRuntime: true,
|
|
7
18
|
});
|
|
@@ -2,21 +2,20 @@
|
|
|
2
2
|
import { ref } from "vue";
|
|
3
3
|
import type { Schemas } from "#shopware";
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
);
|
|
5
|
+
const {
|
|
6
|
+
activeCategory,
|
|
7
|
+
elements,
|
|
8
|
+
level = 0,
|
|
9
|
+
} = defineProps<{
|
|
10
|
+
activeCategory: Schemas["Category"];
|
|
11
|
+
elements: Schemas["Category"][];
|
|
12
|
+
level: number;
|
|
13
|
+
}>();
|
|
15
14
|
|
|
16
15
|
const expandedItems = ref<Set<string>>(new Set());
|
|
17
16
|
|
|
18
17
|
function isActive(navigationElement: Schemas["Category"]) {
|
|
19
|
-
return navigationElement.id ===
|
|
18
|
+
return navigationElement.id === activeCategory?.id;
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
function toggleExpanded(id: string) {
|
|
@@ -32,9 +31,12 @@ function isExpanded(id: string) {
|
|
|
32
31
|
}
|
|
33
32
|
</script>
|
|
34
33
|
<template>
|
|
35
|
-
<div
|
|
34
|
+
<div
|
|
35
|
+
v-if="elements?.length"
|
|
36
|
+
class="self-stretch flex flex-col justify-start items-start gap-4"
|
|
37
|
+
>
|
|
36
38
|
<div
|
|
37
|
-
v-for="(navigationElement, index) in
|
|
39
|
+
v-for="(navigationElement, index) in elements"
|
|
38
40
|
:key="index"
|
|
39
41
|
class="w-full"
|
|
40
42
|
>
|
|
@@ -42,15 +44,18 @@ function isExpanded(id: string) {
|
|
|
42
44
|
:navigation-element="navigationElement"
|
|
43
45
|
:is-active="isActive(navigationElement)"
|
|
44
46
|
:is-expanded="isExpanded(navigationElement.id)"
|
|
45
|
-
:level="
|
|
47
|
+
:level="level"
|
|
46
48
|
@toggle="toggleExpanded(navigationElement.id)"
|
|
47
49
|
/>
|
|
48
50
|
<transition name="filter-collapse">
|
|
49
|
-
<div
|
|
51
|
+
<div
|
|
52
|
+
v-if="navigationElement.children && isExpanded(navigationElement.id)"
|
|
53
|
+
class="self-stretch flex flex-col justify-start items-start"
|
|
54
|
+
>
|
|
50
55
|
<SwCategoryNavigation
|
|
51
56
|
:elements="navigationElement.children"
|
|
52
|
-
:active-category="
|
|
53
|
-
:level="
|
|
57
|
+
:active-category="activeCategory"
|
|
58
|
+
:level="level + 1"
|
|
54
59
|
/>
|
|
55
60
|
</div>
|
|
56
61
|
</transition>
|
|
@@ -60,7 +65,9 @@ function isExpanded(id: string) {
|
|
|
60
65
|
<style scoped>
|
|
61
66
|
.filter-collapse-enter-active,
|
|
62
67
|
.filter-collapse-leave-active {
|
|
63
|
-
transition:
|
|
68
|
+
transition:
|
|
69
|
+
max-height 240ms ease,
|
|
70
|
+
opacity 200ms ease;
|
|
64
71
|
overflow: hidden;
|
|
65
72
|
}
|
|
66
73
|
.filter-collapse-enter-from,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onClickOutside } from "@vueuse/core";
|
|
3
|
+
import { ref, useTemplateRef } from "vue";
|
|
4
|
+
|
|
5
|
+
defineProps<{
|
|
6
|
+
label: string;
|
|
7
|
+
isActive?: boolean;
|
|
8
|
+
}>();
|
|
9
|
+
|
|
10
|
+
const isOpen = ref(false);
|
|
11
|
+
const dropdownElement = useTemplateRef<HTMLDivElement>("dropdownElement");
|
|
12
|
+
|
|
13
|
+
onClickOutside(dropdownElement, () => {
|
|
14
|
+
isOpen.value = false;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function toggle() {
|
|
18
|
+
isOpen.value = !isOpen.value;
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<div ref="dropdownElement" class="relative">
|
|
24
|
+
<!-- Pill button -->
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
class="bg-brand-tertiary rounded-full px-4 py-1.5 inline-flex items-center hover:bg-brand-tertiary-hover transition-colors"
|
|
28
|
+
:class="{ 'ring-2 ring-brand-primary': isActive }"
|
|
29
|
+
@click="toggle"
|
|
30
|
+
:aria-expanded="isOpen"
|
|
31
|
+
aria-haspopup="true"
|
|
32
|
+
>
|
|
33
|
+
<div class="py-1 inline-flex items-center gap-1">
|
|
34
|
+
<span class="text-brand-on-tertiary text-base font-normal leading-6">
|
|
35
|
+
{{ label }}
|
|
36
|
+
</span>
|
|
37
|
+
<SwChevronIcon
|
|
38
|
+
:direction="isOpen ? 'up' : 'down'"
|
|
39
|
+
:size="24"
|
|
40
|
+
class="text-brand-on-tertiary"
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
</button>
|
|
44
|
+
|
|
45
|
+
<!-- Dropdown panel -->
|
|
46
|
+
<div
|
|
47
|
+
v-if="isOpen"
|
|
48
|
+
class="absolute top-full left-0 mt-2 min-w-64 bg-surface-surface rounded-lg shadow-lg ring-1 ring-outline-outline-variant z-50 p-4"
|
|
49
|
+
role="menu"
|
|
50
|
+
>
|
|
51
|
+
<slot />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|
|
@@ -35,7 +35,7 @@ const {
|
|
|
35
35
|
unitPrice,
|
|
36
36
|
displayFromVariants,
|
|
37
37
|
displayFrom,
|
|
38
|
-
|
|
38
|
+
hasListPrice,
|
|
39
39
|
regulationPrice,
|
|
40
40
|
} = useProductPrice(product);
|
|
41
41
|
</script>
|
|
@@ -43,7 +43,7 @@ const {
|
|
|
43
43
|
<template>
|
|
44
44
|
<div :id="product.id" class="inline-flex justify-start items-center gap-2">
|
|
45
45
|
<!-- Sale price display -->
|
|
46
|
-
<div v-if="
|
|
46
|
+
<div v-if="hasListPrice" class="flex items-center gap-2">
|
|
47
47
|
<div class="text-base font-bold leading-normal">
|
|
48
48
|
<SwSharedPrice :value="unitPrice">
|
|
49
49
|
<template #beforePrice>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { 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
6
|
|
|
6
7
|
const props = defineProps<{
|
|
7
8
|
src: string;
|
|
@@ -17,7 +18,8 @@ const gl = {
|
|
|
17
18
|
windowSize: false,
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
const {
|
|
21
|
+
const { state } = await useGLTF(props.src);
|
|
22
|
+
const model = computed(() => state.value?.scene);
|
|
21
23
|
</script>
|
|
22
24
|
<template>
|
|
23
25
|
<TresCanvas v-bind="gl">
|
|
@@ -27,7 +29,7 @@ const { scene: model } = await useGLTF(props.src);
|
|
|
27
29
|
:look-at="[0, 0, 0]"
|
|
28
30
|
/>
|
|
29
31
|
<OrbitControls />
|
|
30
|
-
<primitive :object="model" />
|
|
32
|
+
<primitive v-if="model" :object="model" />
|
|
31
33
|
<TresDirectionalLight :position="[3, 3, 3]" :intensity="1" />
|
|
32
34
|
<TresAmbientLight :intensity="2" />
|
|
33
35
|
</TresCanvas>
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "@shopware/helpers";
|
|
12
12
|
import { getCmsTranslate } from "@shopware/helpers";
|
|
13
13
|
import { defu } from "defu";
|
|
14
|
-
import { computed, ref,
|
|
14
|
+
import { computed, ref, toRef } from "vue";
|
|
15
15
|
import {
|
|
16
16
|
useAddToCart,
|
|
17
17
|
useCartErrorParamsResolver,
|
|
@@ -26,19 +26,17 @@ const { pushSuccess, pushError } = useNotifications();
|
|
|
26
26
|
const { getErrorsCodes } = useCartNotification();
|
|
27
27
|
const { resolveCartError } = useCartErrorParamsResolver();
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
},
|
|
41
|
-
);
|
|
29
|
+
const {
|
|
30
|
+
product: productProp,
|
|
31
|
+
layoutType = "standard",
|
|
32
|
+
displayMode = "standard",
|
|
33
|
+
isProductListing = false,
|
|
34
|
+
} = defineProps<{
|
|
35
|
+
product: Schemas["Product"];
|
|
36
|
+
layoutType?: BoxLayout;
|
|
37
|
+
displayMode?: DisplayMode;
|
|
38
|
+
isProductListing?: boolean;
|
|
39
|
+
}>();
|
|
42
40
|
|
|
43
41
|
type Translations = {
|
|
44
42
|
product: {
|
|
@@ -79,7 +77,7 @@ let translations: Translations = {
|
|
|
79
77
|
|
|
80
78
|
translations = defu(useCmsTranslations(), translations) as Translations;
|
|
81
79
|
|
|
82
|
-
const
|
|
80
|
+
const product = toRef(() => productProp);
|
|
83
81
|
|
|
84
82
|
const { addToCart } = useAddToCart(product);
|
|
85
83
|
|
|
@@ -95,12 +93,12 @@ const toggleWishlistProduct = async () => {
|
|
|
95
93
|
if (!isInWishlist.value) {
|
|
96
94
|
await addToWishlist();
|
|
97
95
|
pushSuccess(
|
|
98
|
-
`${
|
|
96
|
+
`${product?.value.translated.name} ${translations.product.addedToWishlist}`,
|
|
99
97
|
);
|
|
100
98
|
} else {
|
|
101
99
|
await removeFromWishlist();
|
|
102
100
|
pushSuccess(
|
|
103
|
-
`${
|
|
101
|
+
`${product?.value.translated.name} ${translations.product.removedFromTheWishlist}`,
|
|
104
102
|
);
|
|
105
103
|
}
|
|
106
104
|
} catch (error) {
|
|
@@ -109,7 +107,7 @@ const toggleWishlistProduct = async () => {
|
|
|
109
107
|
? `${translations.product.reason}: ${error.details.errors?.[0]?.detail}`
|
|
110
108
|
: "";
|
|
111
109
|
return pushError(
|
|
112
|
-
`${
|
|
110
|
+
`${product?.value.translated.name} ${translations.product.cannotAddToWishlist}\n${reason}`,
|
|
113
111
|
{
|
|
114
112
|
timeout: 5000,
|
|
115
113
|
},
|
|
@@ -131,11 +129,11 @@ const addToCartProxy = async () => {
|
|
|
131
129
|
|
|
132
130
|
if (!errors.length)
|
|
133
131
|
pushSuccess(
|
|
134
|
-
`${
|
|
132
|
+
`${product?.value.translated.name} ${translations.product.addedToCart}`,
|
|
135
133
|
);
|
|
136
134
|
};
|
|
137
135
|
|
|
138
|
-
const fromPrice = getProductFromPrice(
|
|
136
|
+
const fromPrice = getProductFromPrice(product.value);
|
|
139
137
|
const productName = computed(() => getProductName({ product: product.value }));
|
|
140
138
|
const productManufacturer = computed(() =>
|
|
141
139
|
getProductManufacturerName(product.value),
|
|
@@ -165,6 +163,7 @@ const productLink = computed(() =>
|
|
|
165
163
|
:fromPrice="fromPrice"
|
|
166
164
|
:addToCartProxy="addToCartProxy"
|
|
167
165
|
:productLink="productLink"
|
|
166
|
+
:layoutType="layoutType"
|
|
168
167
|
/>
|
|
169
168
|
</div>
|
|
170
|
-
</template>
|
|
169
|
+
</template>
|