@shopware/cms-base-layer 2.1.0 → 3.0.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 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
@@ -104,24 +114,14 @@ export default defineNuxtConfig({
104
114
  ```
105
115
 
106
116
  ```ts [uno.config.ts]
107
- // uno.config.ts
108
- import config from './.nuxt/uno.config.mjs'
109
-
110
- export default config
111
- ```
112
-
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
- ```ts [uno.config.ts]
116
- // uno.config.ts
117
117
  import { mergeConfigs } from '@unocss/core'
118
- import config from './.nuxt/uno.config.mjs'
118
+ import baseConfig from './.nuxt/uno.config.mjs'
119
119
 
120
- export default mergeConfigs([config, {
120
+ export default mergeConfigs([baseConfig, {
121
121
  theme: {
122
122
  colors: {
123
- primary: '#ff3e00',
124
- secondary: '#1c1c1c',
123
+ 'brand-primary': '#ff3e00',
124
+ 'brand-secondary': '#1c1c1c',
125
125
  },
126
126
  },
127
127
  }])
@@ -372,72 +372,52 @@ The preload URL includes the optimized `format` and `quality` parameters from `a
372
372
 
373
373
  ## Responsive CMS Images
374
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).
375
+ Images are optimized to prevent the browser from downloading images larger than their displayed dimensions — a common Lighthouse performance issue.
384
376
 
385
- The browser combines `sizes` + `srcset` to download only the image size it actually needs — during HTML parsing, before any JavaScript runs.
377
+ ### Product Card Images (`SwProductCardImage`)
386
378
 
387
- ### Configuration
388
-
389
- Default slot-count-to-sizes mappings are set in `app.config.ts` and can be overridden:
379
+ 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
380
 
391
381
  ```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
- });
382
+ // nuxt.config.ts
383
+ productCard: {
384
+ modifiers: { format: "webp", quality: 90, fit: "cover" },
385
+ }
401
386
  ```
402
387
 
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
- });
388
+ ```vue
389
+ <NuxtImg preset="productCard"
390
+ :src="coverSrcPath"
391
+ width="400" height="400"
392
+ densities="1x"
393
+ loading="lazy" />
414
394
  ```
415
395
 
416
- ### Per-block override
396
+ - **Fixed `width`/`height`** (400px) — avoid hydration mismatches caused by dynamic DOM measurement
397
+ - **`densities="1x"`** — prevents duplicate retina requests
398
+ - **`loading="lazy"`** — defers off-viewport images
417
399
 
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`.
400
+ > **⚠️ Avoid** adding `decoding` or `sizes` props on the component they trigger Vue hydration attribute mismatches with NuxtImg, which cause duplicate image requests.
419
401
 
420
- ### Synthetic srcset fallback
402
+ ### CMS Images (`CmsElementImage`)
421
403
 
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:
404
+ CMS image elements use `useElementSize()` to measure the rendered container and pass the size to `<NuxtImg>` via `width`/`height` props:
423
405
 
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
- ```
406
+ - During SSR, no image is fetched (size is `undefined`)
407
+ - After hydration, the container is measured and a single correctly-sized image is requested
408
+ - The size is multiplied by 2 (for retina) and rounded up to the nearest 100px
409
+
410
+ ### Slider Components
433
411
 
434
- The `format` and `quality` values are taken from the `backgroundImage` config in `app.config.ts`.
412
+ **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
413
 
436
- > **Note:** Synthetic srcset requires CDN-based image resizing support. See the [Image Optimization](#%EF%B8%8F-image-optimization) section for requirements.
414
+ ### LCP Image Preloading
415
+
416
+ **`useLcpImagePreload`** scans CMS sections for the first image and injects `<link rel="preload" as="image" fetchpriority="high">` during SSR.
437
417
 
438
418
  ## 🔄 UnoCSS Runtime
439
419
 
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).
420
+ 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
421
 
442
422
  The runtime is **enabled by default**. To disable it, set `unocssRuntime` to `false` in your project's `app.config.ts`:
443
423
 
@@ -494,42 +474,36 @@ No additional packages needed to be installed.
494
474
 
495
475
  Full changelog for stable version is available [here](https://github.com/shopware/frontends/blob/main/packages/cms-base-layer/CHANGELOG.md)
496
476
 
497
- ### Latest changes: 2.1.0
498
-
499
- ### Minor Changes
477
+ ### Latest changes: 3.0.0
500
478
 
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
479
+ ### Major Changes
503
480
 
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.
481
+ - [#2406](https://github.com/shopware/frontends/pull/2406) [`df93461`](https://github.com/shopware/frontends/commit/df93461434cb79ec9d722cdbd42a37a9af07fb03) Thanks [@mdanilowicz](https://github.com/mdanilowicz)! - Remove bundled UnoCSS configuration and design tokens from the CMS layer. Consumers who relied on the previous default UnoCSS setup should extend `@shopware/unocss-design-tokens-layer` alongside this package. See the package README and framework docs for migration steps.
505
482
 
506
- Includes new `SwFilterDropdown` and `SwProductListingFiltersHorizontal` components, with a `displayMode` prop added to all filter components to support both layouts.
483
+ ### Minor Changes
507
484
 
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
485
+ - [#2420](https://github.com/shopware/frontends/pull/2420) [`9e37ab6`](https://github.com/shopware/frontends/commit/9e37ab6897f501ed3d261fa619aee349e46342c2) Thanks [@mdanilowicz](https://github.com/mdanilowicz)! - Add FrontendAccountCustomerGroupRegistrationPage component for customer group view
509
486
 
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
487
+ ### Patch Changes
511
488
 
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
489
+ - [#2439](https://github.com/shopware/frontends/pull/2439) [`a992e39`](https://github.com/shopware/frontends/commit/a992e396ea6aa3b44783d183561fa8b0605b77f0) Thanks [@patzick](https://github.com/patzick)! - Use Nuxt route composables in CMS components to keep route query access available during SSR.
513
490
 
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.
491
+ - [#2389](https://github.com/shopware/frontends/pull/2389) [`05438c6`](https://github.com/shopware/frontends/commit/05438c636a6c99b48e87d8f2ff5b03bf313c4e67) Thanks [@mkucmus](https://github.com/mkucmus)! - Fix product card and CMS image sizing to prevent duplicate/oversized image requests. Move fixed dimensions and `densities="1x"` into the `productCard` preset, and use `useElementSize`-based `width`/`height` props for `CmsElementImage`.
515
492
 
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
493
+ - [#2378](https://github.com/shopware/frontends/pull/2378) [`c36bc1f`](https://github.com/shopware/frontends/commit/c36bc1ff17e8e34c52fa91e6388ce210fffb7e8e) Thanks [@patzick](https://github.com/patzick)! - Add UnoCSS directive transformation for CMS block styles so layered Nuxt apps do not emit CSS minification warnings from raw `@apply` directives during production builds.
517
494
 
518
- ### Patch Changes
495
+ - [#2369](https://github.com/shopware/frontends/pull/2369) [`3c16985`](https://github.com/shopware/frontends/commit/3c16985ddf3878bc207c514a5ab8e4a6409f809c) Thanks [@mkucmus](https://github.com/mkucmus)! - Fixed `xss` library loading issue in Vite dev server by adding it to `optimizeDeps.include`
519
496
 
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.
497
+ - [#2371](https://github.com/shopware/frontends/pull/2371) [`33e0c69`](https://github.com/shopware/frontends/commit/33e0c69afc3de854733ab61f866ba65cce1489f6) Thanks [@patzick](https://github.com/patzick)! - Disable automatic CMS LCP image preload by default.
521
498
 
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
499
+ The preload helper now only injects image preload tags when
500
+ `appConfig.lcpImagePreload` is explicitly enabled, which avoids noisy preload
501
+ warnings on storefront pages that do not immediately use the detected image.
525
502
 
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.
503
+ - [#2326](https://github.com/shopware/frontends/pull/2326) [`e7efff8`](https://github.com/shopware/frontends/commit/e7efff8c615ae8d0858572933285216cc533dd0b) Thanks [@mdanilowicz](https://github.com/mdanilowicz)! - Gate wishlist button behind login when useLoginModal is provided via provide/inject. For guests, show login modal on wishlist click and add product to wishlist after successful login.
529
504
 
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`.
505
+ - [#2346](https://github.com/shopware/frontends/pull/2346) [`a47143a`](https://github.com/shopware/frontends/commit/a47143a670f49deecc35dce4bb8b6bd12d9a3b47) Thanks [@joberthel](https://github.com/joberthel)! - Improve `SwMedia3D` model framing by fitting loaded 3D models into the viewport automatically. This fixes cases where very small 3D files appeared tiny and hard to inspect.
531
506
 
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
507
+ - Updated dependencies [[`22fc8a7`](https://github.com/shopware/frontends/commit/22fc8a7301f6a7d2612d907ab73555978b651c00), [`bea7f58`](https://github.com/shopware/frontends/commit/bea7f5882cb58c6d47c84a82db5c8ecaf9bcf8ef), [`b8c0091`](https://github.com/shopware/frontends/commit/b8c00913c3afb5e1e63de9565105f8f8e3bf299f)]:
508
+ - @shopware/helpers@1.7.1
509
+ - @shopware/composables@1.11.1
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,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,23 @@ 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
32
  <TresPerspectiveCamera
27
33
  :args="[75, 1, 0.1, 2000]"
28
34
  :position="[0, 0, 500]"
29
- :look-at="[0, 0, 0]"
30
35
  />
31
- <OrbitControls />
32
- <primitive v-if="model" :object="model" />
36
+ <OrbitControls make-default />
37
+ <Bounds ref="boundsRef" clip use-mounted>
38
+ <primitive v-if="model" :object="model" @click="focusObject" />
39
+ </Bounds>
33
40
  <TresDirectionalLight :position="[3, 3, 3]" :intensity="1" />
34
41
  <TresAmbientLight :intensity="2" />
35
42
  </TresCanvas>
@@ -50,6 +50,8 @@ type Translations = {
50
50
  badges: {
51
51
  topseller: string;
52
52
  };
53
+ addToWishlist: string;
54
+ removeFromWishlist: string;
53
55
  };
54
56
  errors: {
55
57
  [key: string]: string;
@@ -68,6 +70,8 @@ let translations: Translations = {
68
70
  badges: {
69
71
  topseller: "Tip",
70
72
  },
73
+ addToWishlist: "Add to wishlist",
74
+ removeFromWishlist: "Remove from wishlist",
71
75
  },
72
76
  errors: {
73
77
  "product-stock-reached":
@@ -5,9 +5,8 @@ import {
5
5
  isProductOnSale,
6
6
  isProductTopSeller,
7
7
  } from "@shopware/helpers";
8
- import { useElementSize } from "@vueuse/core";
9
- import { computed, useTemplateRef } from "vue";
10
- import { useImagePlaceholder } from "#imports";
8
+ import { computed, inject } from "vue";
9
+ import { useUser } from "#imports";
11
10
  import type { Schemas } from "#shopware";
12
11
 
13
12
  type Translations = {
@@ -15,6 +14,8 @@ type Translations = {
15
14
  badges: {
16
15
  topseller: string;
17
16
  };
17
+ addToWishlist: string;
18
+ removeFromWishlist: string;
18
19
  };
19
20
  };
20
21
 
@@ -27,32 +28,13 @@ const props = defineProps<{
27
28
  productLink: UrlRouteOutput;
28
29
  }>();
29
30
 
30
- const containerElement = useTemplateRef<HTMLDivElement>("containerElement");
31
- const { width, height } = useElementSize(containerElement);
32
-
33
- const DEFAULT_THUMBNAIL_SIZE = 10;
34
- function roundUp(num: number) {
35
- return num ? Math.ceil(num / 100) * 100 : DEFAULT_THUMBNAIL_SIZE;
36
- }
37
-
38
31
  const coverSrcPath = computed(() => {
39
32
  return (
40
- getSmallestThumbnailUrl(props.product?.cover?.media) ||
41
- props.product?.cover?.media?.url
33
+ props.product?.cover?.media?.url ||
34
+ getSmallestThumbnailUrl(props.product?.cover?.media)
42
35
  );
43
36
  });
44
37
 
45
- const imageModifiers = computed(() => {
46
- // Use the larger dimension and apply 2x for high-DPI displays
47
- // For square containers, width and height should be the same
48
- const containerSize = Math.max(width.value || 0, height.value || 0);
49
- const size = roundUp(containerSize * 2);
50
- return {
51
- width: size,
52
- height: size,
53
- };
54
- });
55
-
56
38
  const coverAlt = computed(() => {
57
39
  return props.product?.cover?.media?.alt || props.product?.translated?.name;
58
40
  });
@@ -60,16 +42,30 @@ const coverAlt = computed(() => {
60
42
  const isOnSale = computed(() => isProductOnSale(props.product));
61
43
  const isTopseller = computed(() => isProductTopSeller(props.product));
62
44
 
63
- const placeholderSvg = useImagePlaceholder();
45
+ const { isLoggedIn } = useUser();
46
+ const loginModal = inject<{
47
+ open: (options?: { onSuccess?: () => void | Promise<void> }) => void;
48
+ } | null>("loginModal", null);
49
+
50
+ function handleWishlistClick() {
51
+ if (isLoggedIn.value || !loginModal) {
52
+ props.toggleWishlist();
53
+ return;
54
+ }
55
+ loginModal.open({ onSuccess: props.toggleWishlist });
56
+ }
64
57
  </script>
65
58
 
66
59
  <template>
67
- <div ref="containerElement" class="self-stretch min-h-[350px] relative flex flex-col justify-start items-start overflow-hidden aspect-square">
60
+ <div class="self-stretch min-h-[350px] relative flex flex-col justify-start items-start overflow-hidden aspect-square">
68
61
  <RouterLink :to="productLink" class="self-stretch h-full relative overflow-hidden">
69
62
  <NuxtImg preset="productCard"
70
63
  class="w-full h-full absolute top-0 left-0 object-cover"
71
- :placeholder="placeholderSvg"
72
- :src="coverSrcPath" :alt="coverAlt" :modifiers="imageModifiers" data-testid="product-box-img" />
64
+ :src="coverSrcPath" :alt="coverAlt"
65
+ width="400" height="400"
66
+ densities="1x"
67
+ loading="lazy"
68
+ data-testid="product-box-img" />
73
69
  </RouterLink>
74
70
 
75
71
  <div v-if="isTopseller || isOnSale"
@@ -80,9 +76,11 @@ const placeholderSvg = useImagePlaceholder();
80
76
  </div>
81
77
 
82
78
  <client-only>
83
- <SwIconButton type="secondary" aria-label="Toggle wishlist" :disabled="isLoading"
79
+ <SwIconButton type="secondary"
80
+ :aria-label="isInWishlist ? translations.product.removeFromWishlist : translations.product.addToWishlist"
81
+ :disabled="isLoading"
84
82
  class="w-10 h-10 right-4 top-4 absolute bg-brand-secondary rounded-full flex items-center justify-center"
85
- data-testid="product-box-toggle-wishlist-button" @click="toggleWishlist">
83
+ data-testid="product-box-toggle-wishlist-button" @click="handleWishlistClick">
86
84
  <SwWishlistIcon :filled="isInWishlist" />
87
85
  </SwIconButton>
88
86
  </client-only>
@@ -7,8 +7,8 @@ import { useCmsTranslations } from "@shopware/composables";
7
7
  import { defu } from "defu";
8
8
  import { computed, reactive } from "vue";
9
9
  import type { ComputedRef, UnwrapNestedRefs } from "vue";
10
- import { type LocationQueryRaw, useRoute, useRouter } from "vue-router";
11
- import { useCategoryListing } from "#imports";
10
+ import type { LocationQueryRaw } from "vue-router";
11
+ import { useCategoryListing, useRoute, useRouter } from "#imports";
12
12
  import type { Schemas, operations } from "#shopware";
13
13
 
14
14
  const props = defineProps<{
@@ -7,8 +7,8 @@ import { useCmsTranslations } from "@shopware/composables";
7
7
  import { defu } from "defu";
8
8
  import { computed, reactive } from "vue";
9
9
  import type { ComputedRef, UnwrapNestedRefs } from "vue";
10
- import { type LocationQueryRaw, useRoute, useRouter } from "vue-router";
11
- import { useCategoryListing } from "#imports";
10
+ import type { LocationQueryRaw } from "vue-router";
11
+ import { useCategoryListing, useRoute, useRouter } from "#imports";
12
12
  import type { Schemas, operations } from "#shopware";
13
13
 
14
14
  const props = defineProps<{
@@ -4,8 +4,7 @@ import { buildUrlPrefix, getProductRoute } from "@shopware/helpers";
4
4
  import { defu } from "defu";
5
5
  import { computed, ref, unref } from "vue";
6
6
  import type { ComputedRef } from "vue";
7
- import { useRouter } from "vue-router";
8
- import { useProductConfigurator, useUrlResolver } from "#imports";
7
+ import { useProductConfigurator, useRouter, useUrlResolver } from "#imports";
9
8
  import type { Schemas } from "#shopware";
10
9
 
11
10
  const { getUrlPrefix } = useUrlResolver();
@@ -4,16 +4,16 @@ import {
4
4
  getBackgroundImageUrl,
5
5
  getCmsLayoutConfiguration,
6
6
  } from "@shopware/helpers";
7
- import { h, provide } from "vue";
8
- import { useAppConfig } from "#imports";
7
+ import { h, provide, resolveComponent } from "vue";
9
8
  import type { Schemas } from "#shopware";
9
+ import { useTypedAppConfig } from "../../../composables/useTypedAppConfig";
10
10
  import { getImageSizes } from "../../../helpers/cms/getImageSizes";
11
11
 
12
12
  const props = defineProps<{
13
13
  content: Schemas["CmsBlock"];
14
14
  }>();
15
15
 
16
- const appConfig = useAppConfig();
16
+ const appConfig = useTypedAppConfig();
17
17
 
18
18
  const slotCount = props.content.slots?.length || 1;
19
19
  provide("cms-block-slot-count", slotCount);
@@ -67,7 +67,12 @@ const DynamicRender = () => {
67
67
  }),
68
68
  );
69
69
  }
70
- console.error(`Component not resolve: ${componentNameToResolve}`);
70
+ if (import.meta.dev) {
71
+ console.warn(
72
+ `[CMS] Block type "${componentName}" is not implemented.\n → Create a component named "${componentNameToResolve}.vue" to render it.\n 📖 Docs: https://frontends.shopware.com/getting-started/cms/create-blocks`,
73
+ );
74
+ return h(resolveComponent("CmsNoComponent"), { content: props.content });
75
+ }
71
76
  return h("div", {}, "");
72
77
  };
73
78
  </script>
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { resolveCmsComponent } from "@shopware/composables";
3
3
  import { getCmsLayoutConfiguration } from "@shopware/helpers";
4
- import { h } from "vue";
4
+ import { h, resolveComponent } from "vue";
5
5
  import type { Schemas } from "#shopware";
6
6
 
7
7
  const props = defineProps<{
@@ -28,7 +28,12 @@ const DynamicRender = () => {
28
28
  class: cssClasses,
29
29
  });
30
30
  }
31
- console.error(`Component not resolved: ${componentNameToResolve}`);
31
+ if (import.meta.dev) {
32
+ console.warn(
33
+ `[CMS] Element type "${componentName}" is not implemented.\n → Create a component named "${componentNameToResolve}.vue" to render it.\n 📖 Docs: https://frontends.shopware.com/getting-started/cms/create-elements`,
34
+ );
35
+ return h(resolveComponent("CmsNoComponent"), { content: props.content });
36
+ }
32
37
  return h("div", {}, "");
33
38
  };
34
39
  </script>
@@ -1,9 +1,11 @@
1
1
  <script setup lang="ts">
2
+ import { refAutoReset } from "@vueuse/core";
3
+ import { pascalCase } from "scule";
2
4
  import { computed } from "vue";
3
5
  import type { Schemas } from "#shopware";
4
6
 
5
7
  const props = defineProps<{
6
- content: Schemas["CmsBlock"];
8
+ content: Schemas["CmsSection"] | Schemas["CmsBlock"] | Schemas["CmsSlot"];
7
9
  }>();
8
10
 
9
11
  const elementType = computed(() =>
@@ -15,13 +17,90 @@ const elementType = computed(() =>
15
17
  );
16
18
 
17
19
  const componentName = computed(() => props.content.type);
20
+
21
+ const expectedComponentName = computed(() =>
22
+ pascalCase(`Cms-${elementType.value}-${componentName.value ?? ""}`),
23
+ );
24
+
25
+ const docsUrl = computed(() => {
26
+ const params = new URLSearchParams({
27
+ component: expectedComponentName.value,
28
+ type: elementType.value.toLowerCase(),
29
+ });
30
+ return `https://frontends.shopware.com/getting-started/cms/missing-component?${params}`;
31
+ });
32
+
33
+ const aiPrompt = computed(() => {
34
+ const schemaType =
35
+ props.content.apiAlias === "cms_block"
36
+ ? 'Schemas["CmsBlock"]'
37
+ : props.content.apiAlias === "cms_section"
38
+ ? 'Schemas["CmsSection"]'
39
+ : 'Schemas["CmsSlot"]';
40
+
41
+ const contentJson = JSON.stringify(props.content, null, 2);
42
+ const type = elementType.value.toLowerCase();
43
+ const name = componentName.value;
44
+ const compName = expectedComponentName.value;
45
+
46
+ return [
47
+ `Create a Vue 3 component \`${compName}.vue\` for a Shopware Frontends headless storefront.`,
48
+ "",
49
+ `This component renders the CMS ${type} type: "${name}" (apiAlias: "${props.content.apiAlias}").`,
50
+ "",
51
+ "Requirements:",
52
+ `- Accept a \`content: ${schemaType}\` prop`,
53
+ `- Render the data for CMS ${type} type "${name}"`,
54
+ `- Follow patterns from existing Cms${elementType.value} components in packages/cms-base-layer/app/components/public/cms/${type}/`,
55
+ "- Use composables from @shopware/composables where applicable",
56
+ "",
57
+ `The full content prop for this ${type} is:`,
58
+ contentJson,
59
+ "",
60
+ `Reference: ${docsUrl.value}`,
61
+ ].join("\n");
62
+ });
63
+
64
+ const copied = refAutoReset(false, 2000);
65
+
66
+ async function copyPrompt() {
67
+ try {
68
+ await navigator.clipboard.writeText(aiPrompt.value);
69
+ copied.value = true;
70
+ } catch {
71
+ // Fallback: clipboard API unavailable (non-secure origin, unfocused document)
72
+ console.warn("[CMS] Could not copy to clipboard. Prompt logged below:");
73
+ console.info(aiPrompt.value);
74
+ }
75
+ }
18
76
  </script>
19
- Ū
77
+
20
78
  <template>
21
- <span class="sw-text-error">
22
- <b>
23
- {{ elementType }}<i> {{ componentName }} </i>
24
- </b>
25
- is not implemented yet!
26
- </span>
79
+ <div
80
+ class="sw-cms-no-component box-border min-h-[40px] rounded border-2 border-dashed border-brand-primary bg-brand-secondary font-mono text-[11px] text-brand-on-secondary"
81
+ >
82
+ <div class="flex flex-wrap items-center gap-1.5 px-2 py-1.5">
83
+ <span class="text-brand-primary">⚠ missing implementation:</span>
84
+ <span class="font-semibold">{{ expectedComponentName }}</span>
85
+
86
+ <a
87
+ :href="docsUrl"
88
+ target="_blank"
89
+ rel="noopener noreferrer"
90
+ title="View CMS documentation"
91
+ class="whitespace-nowrap rounded border border-outline-outline-primary bg-surface-surface px-[5px] py-px text-[10px] leading-none text-brand-primary no-underline hover:bg-brand-secondary-hover"
92
+ >
93
+ docs ↗
94
+ </a>
95
+
96
+ <button
97
+ type="button"
98
+ title="Copy AI prompt to clipboard"
99
+ class="cursor-pointer whitespace-nowrap rounded border border-outline-outline-primary bg-surface-surface px-[5px] py-px font-mono text-[10px] leading-none text-brand-primary hover:bg-brand-secondary-hover"
100
+ @click.stop="copyPrompt"
101
+ >
102
+ {{ copied ? "copied ✓" : "copy AI prompt" }}
103
+ </button>
104
+ </div>
105
+ </div>
27
106
  </template>
@@ -6,20 +6,17 @@ import {
6
6
  } from "@shopware/helpers";
7
7
  import { pascalCase } from "scule";
8
8
  import { computed, h, resolveComponent, watchEffect } from "vue";
9
- import {
10
- createCategoryListingContext,
11
- useAppConfig,
12
- useNavigationContext,
13
- } from "#imports";
9
+ import { createCategoryListingContext, useNavigationContext } from "#imports";
14
10
  import type { Schemas } from "#shopware";
15
11
  import { useLcpImagePreload } from "../../../composables/useLcpImagePreload";
12
+ import { useTypedAppConfig } from "../../../composables/useTypedAppConfig";
16
13
 
17
14
  const props = defineProps<{
18
15
  content: Schemas["CmsPage"];
19
16
  }>();
20
17
 
21
18
  const { routeName } = useNavigationContext();
22
- const appConfig = useAppConfig();
19
+ const appConfig = useTypedAppConfig();
23
20
 
24
21
  // Function to initialize or update listing context
25
22
  function updateListingContext(content: Schemas["CmsPage"]) {
@@ -0,0 +1,52 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ navigationId: string;
4
+ }>();
5
+
6
+ const { apiClient } = useShopwareContext();
7
+
8
+ const { data: registrationResponse } = await useAsyncData(
9
+ `cmsNavigation${props.navigationId}`,
10
+ async () => {
11
+ const response = await apiClient.invoke(
12
+ "getCustomerGroupRegistrationInfo get /customer-group-registration/config/{customerGroupId}",
13
+ {
14
+ pathParams: { customerGroupId: props.navigationId },
15
+ },
16
+ );
17
+ return response.data || {};
18
+ },
19
+ );
20
+
21
+ useSeoMeta({
22
+ description: () =>
23
+ registrationResponse.value?.translated?.registrationSeoMetaDescription ||
24
+ undefined,
25
+ });
26
+ </script>
27
+
28
+ <template>
29
+ <div class="container mx-auto bg-surface-surface flex flex-col mt-10">
30
+ <h1
31
+ class="mb-4 text-2xl font-extrabold leading-none tracking-tight text-surface-on-surface md:text-3xl lg:text-5xl text-center"
32
+ >
33
+ {{ registrationResponse?.translated.registrationTitle }}
34
+ </h1>
35
+ <div
36
+ v-if="registrationResponse?.registrationActive"
37
+ class="text-lg font-normal text-surface-on-surface-variant lg:text-xl"
38
+ >
39
+ <div
40
+ v-if="registrationResponse?.translated.registrationIntroduction"
41
+ class="px-6 sm:px-4 mb-6"
42
+ v-html="registrationResponse.translated.registrationIntroduction"
43
+ />
44
+ <AccountRegistrationForm
45
+ :customer-group-id="registrationResponse?.id"
46
+ :company-only="
47
+ registrationResponse?.translated?.registrationOnlyCompanyRegistration
48
+ "
49
+ />
50
+ </div>
51
+ </div>
52
+ </template>
@@ -3,14 +3,10 @@ import type {
3
3
  CmsElementImage,
4
4
  CmsElementManufacturerLogo,
5
5
  } from "@shopware/composables";
6
- import {
7
- buildCdnImageUrl,
8
- buildUrlPrefix,
9
- generateCdnSrcSet,
10
- } from "@shopware/helpers";
6
+ import { buildUrlPrefix } from "@shopware/helpers";
11
7
  import { useElementSize } from "@vueuse/core";
12
- import { computed, defineAsyncComponent, inject, useTemplateRef } from "vue";
13
- import { useAppConfig, useCmsElementImage, useUrlResolver } from "#imports";
8
+ import { computed, defineAsyncComponent, useTemplateRef } from "vue";
9
+ import { useCmsElementImage, useUrlResolver } from "#imports";
14
10
  import { isSpatial } from "../../../../helpers/media/isSpatial";
15
11
 
16
12
  const props = defineProps<{
@@ -29,34 +25,17 @@ const {
29
25
  mimeType,
30
26
  } = useCmsElementImage(props.content);
31
27
 
32
- const imageSizes = inject<string>("cms-image-sizes", "100vw");
33
- const appConfig = useAppConfig();
34
-
35
28
  const imageElement = useTemplateRef<HTMLImageElement>("imageElement");
36
29
  const { width, height } = useElementSize(imageElement);
37
30
 
38
- const cdnOptions = computed(() => ({
39
- format: appConfig.backgroundImage?.format,
40
- quality: appConfig.backgroundImage?.quality,
41
- }));
42
-
43
- const srcSet = computed(
44
- () =>
45
- imageAttrs.value.srcset ||
46
- generateCdnSrcSet(imageAttrs.value.src, undefined, cdnOptions.value),
47
- );
31
+ function roundUp(num: number) {
32
+ return Math.ceil(num / 100) * 100;
33
+ }
48
34
 
49
- const srcPath = computed(() => {
50
- // Only add dimension params after mount to avoid hydration mismatch
51
- // (useElementSize returns 0 during SSR). The srcset handles responsive loading.
52
- if (width.value || height.value) {
53
- return buildCdnImageUrl(
54
- imageAttrs.value.src,
55
- { width: width.value, height: height.value },
56
- cdnOptions.value,
57
- );
58
- }
59
- return imageAttrs.value.src || "";
35
+ const imageSize = computed(() => {
36
+ const containerSize = Math.max(width.value || 0, height.value || 0);
37
+ if (!containerSize) return undefined;
38
+ return roundUp(containerSize * 2);
60
39
  });
61
40
 
62
41
  const imageComputedContainerAttrs = computed(() => {
@@ -110,7 +89,8 @@ const SwMedia3D = computed(() => {
110
89
  ref="imageElement"
111
90
  preset="productDetail"
112
91
  loading="lazy"
113
- :sizes="imageSizes"
92
+ :width="imageSize"
93
+ :height="imageSize"
114
94
  :class="{
115
95
  'w-full': !imageGallery,
116
96
  'h-full': !imageGallery && ['cover', 'stretch'].includes(displayMode),
@@ -120,8 +100,7 @@ const SwMedia3D = computed(() => {
120
100
  'object-contain': imageGallery || displayMode !== 'cover',
121
101
  }"
122
102
  :alt="imageAttrs.alt"
123
- :src="srcPath"
124
- :srcset="srcSet"
103
+ :src="imageAttrs.src"
125
104
  />
126
105
  </component>
127
106
  </template>
@@ -3,8 +3,12 @@ import type { CmsElementProductListing } from "@shopware/composables";
3
3
  import { useCmsTranslations } from "@shopware/composables";
4
4
  import { defu } from "defu";
5
5
  import { computed, ref, useTemplateRef, watch } from "vue";
6
- import { useRoute, useRouter } from "vue-router";
7
- import { useCategoryListing, useCmsElementConfig } from "#imports";
6
+ import {
7
+ useCategoryListing,
8
+ useCmsElementConfig,
9
+ useRoute,
10
+ useRouter,
11
+ } from "#imports";
8
12
  import type { Schemas, operations } from "#shopware";
9
13
 
10
14
  const props = defineProps<{
@@ -1,4 +1,4 @@
1
- import { useAppConfig } from "nuxt/app";
1
+ import { useTypedAppConfig } from "./useTypedAppConfig";
2
2
 
3
3
  /**
4
4
  * Composable that provides an SVG placeholder image as a data URI
@@ -8,7 +8,7 @@ import { useAppConfig } from "nuxt/app";
8
8
  * @returns Base64-encoded SVG data URI
9
9
  */
10
10
  export function useImagePlaceholder(color?: string) {
11
- const appConfig = useAppConfig();
11
+ const appConfig = useTypedAppConfig();
12
12
  const placeholderColor =
13
13
  color || appConfig.imagePlaceholder?.color || "#543B95";
14
14
 
@@ -1,7 +1,8 @@
1
1
  import { computed } from "vue";
2
- import { useAppConfig, useHead } from "#imports";
2
+ import { useHead } from "#imports";
3
3
  import type { Schemas } from "#shopware";
4
4
  import { findFirstCmsImageUrl } from "../helpers/cms/findFirstCmsImageUrl";
5
+ import { useTypedAppConfig } from "./useTypedAppConfig";
5
6
 
6
7
  /**
7
8
  * Preloads the first image found in CMS sections (background or element).
@@ -11,13 +12,16 @@ import { findFirstCmsImageUrl } from "../helpers/cms/findFirstCmsImageUrl";
11
12
  * allowing the browser to fetch the image before parsing CSS.
12
13
  */
13
14
  export function useLcpImagePreload(sections: Schemas["CmsSection"][]) {
14
- const appConfig = useAppConfig();
15
+ const appConfig = useTypedAppConfig();
16
+ const isEnabled = Boolean(appConfig.lcpImagePreload);
15
17
 
16
18
  const lcpImageHref = computed(() =>
17
- findFirstCmsImageUrl(sections, {
18
- format: appConfig.backgroundImage?.format,
19
- quality: appConfig.backgroundImage?.quality,
20
- }),
19
+ isEnabled
20
+ ? findFirstCmsImageUrl(sections, {
21
+ format: appConfig.backgroundImage?.format,
22
+ quality: appConfig.backgroundImage?.quality,
23
+ })
24
+ : undefined,
21
25
  );
22
26
 
23
27
  useHead(
@@ -0,0 +1,15 @@
1
+ import type { BackgroundImageOptions } from "@shopware/helpers";
2
+ import { useAppConfig } from "#imports";
3
+
4
+ type CmsBaseLayerAppConfig = ReturnType<typeof useAppConfig> & {
5
+ imagePlaceholder?: {
6
+ color?: string;
7
+ };
8
+ backgroundImage?: BackgroundImageOptions;
9
+ imageSizes?: Record<string, string>;
10
+ lcpImagePreload?: boolean;
11
+ };
12
+
13
+ export function useTypedAppConfig() {
14
+ return useAppConfig() as CmsBaseLayerAppConfig;
15
+ }
package/index.d.ts CHANGED
@@ -3,34 +3,42 @@
3
3
  export * from "@shopware/composables";
4
4
  export * from "./.nuxt/imports";
5
5
 
6
+ type CmsBaseLayerAppConfig = {
7
+ /** Placeholder shown while CMS images are loading */
8
+ imagePlaceholder?: {
9
+ /** CSS color value for the placeholder background */
10
+ color?: string;
11
+ };
12
+ /** CDN optimization options applied to CMS background images and synthetic srcsets */
13
+ backgroundImage?: {
14
+ /** Output format passed to the CDN (e.g. "webp", "avif") */
15
+ format?: string;
16
+ /** Image quality 1-100 passed to the CDN */
17
+ quality?: number;
18
+ };
19
+ /** Preload the first CMS image as a likely LCP candidate */
20
+ lcpImagePreload?: boolean;
21
+ /**
22
+ * Maps CMS block slot count to a responsive `sizes` attribute value.
23
+ * Used by CmsGenericBlock to provide sizing hints to child image elements.
24
+ * @example
25
+ * ```ts
26
+ * imageSizes: {
27
+ * 1: "(max-width: 768px) 100vw, 100vw",
28
+ * 2: "(max-width: 768px) 100vw, 50vw",
29
+ * default: "(max-width: 768px) 50vw, 25vw",
30
+ * }
31
+ * ```
32
+ */
33
+ imageSizes?: Record<string | number, string>;
34
+ };
35
+
6
36
  declare module "nuxt/schema" {
7
- interface AppConfig {
8
- /** Placeholder shown while CMS images are loading */
9
- imagePlaceholder?: {
10
- /** CSS color value for the placeholder background */
11
- color?: string;
12
- };
13
- /** CDN optimization options applied to CMS background images and synthetic srcsets */
14
- backgroundImage?: {
15
- /** Output format passed to the CDN (e.g. "webp", "avif") */
16
- format?: string;
17
- /** Image quality 1-100 passed to the CDN */
18
- quality?: number;
19
- };
20
- /**
21
- * Maps CMS block slot count to a responsive `sizes` attribute value.
22
- * Used by CmsGenericBlock to provide sizing hints to child image elements.
23
- * @example
24
- * ```ts
25
- * imageSizes: {
26
- * 1: "(max-width: 768px) 100vw, 100vw",
27
- * 2: "(max-width: 768px) 100vw, 50vw",
28
- * default: "(max-width: 768px) 50vw, 25vw",
29
- * }
30
- * ```
31
- */
32
- imageSizes?: Record<string | number, string>;
33
- /** Enable UnoCSS runtime for dynamic class generation */
34
- unocssRuntime?: boolean;
35
- }
37
+ interface AppConfig extends CmsBaseLayerAppConfig {}
38
+ interface AppConfigInput extends CmsBaseLayerAppConfig {}
39
+ }
40
+
41
+ declare module "@nuxt/schema" {
42
+ interface AppConfig extends CmsBaseLayerAppConfig {}
43
+ interface AppConfigInput extends CmsBaseLayerAppConfig {}
36
44
  }
package/nuxt.config.ts CHANGED
@@ -111,6 +111,11 @@ export default defineNuxtConfig({
111
111
  build: {
112
112
  transpile: ["@shopware/cms-base-layer"],
113
113
  },
114
+ vite: {
115
+ optimizeDeps: {
116
+ include: ["xss"],
117
+ },
118
+ },
114
119
  telemetry: {
115
120
  enabled: false,
116
121
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopware/cms-base-layer",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "Vue CMS Nuxt Layer for Shopware",
5
5
  "author": "Shopware",
6
6
  "repository": {
@@ -34,36 +34,34 @@
34
34
  "dependencies": {
35
35
  "@iconify-json/carbon": "1.2.18",
36
36
  "@nuxt/image": "2.0.0",
37
- "@nuxt/kit": "4.2.2",
38
- "@tresjs/cientos": "5.2.4",
39
- "@tresjs/nuxt": "^5.1.6",
40
- "@unocss/nuxt": "66.6.0",
41
- "@unocss/runtime": "66.6.0",
37
+ "@nuxt/kit": "4.4.6",
38
+ "@tresjs/cientos": "5.7.0",
39
+ "@tresjs/nuxt": "^5.6.0",
42
40
  "@vuelidate/core": "2.0.3",
43
41
  "@vuelidate/validators": "2.0.4",
44
42
  "@vueuse/core": "14.1.0",
45
43
  "entities": "6.0.0",
44
+ "scule": "1.3.0",
46
45
  "html-to-ast": "0.0.6",
47
- "three": "0.182.0",
48
- "unocss": "66.6.0",
49
- "vue": "3.5.27",
46
+ "three": "0.183.2",
47
+ "vue": "3.5.34",
50
48
  "xss": "1.0.15",
51
49
  "@shopware/api-client": "1.5.0",
52
- "@shopware/composables": "1.11.0",
53
- "@shopware/helpers": "1.7.0"
50
+ "@shopware/composables": "1.11.1",
51
+ "@shopware/helpers": "1.7.1"
54
52
  },
55
53
  "devDependencies": {
56
54
  "@biomejs/biome": "1.8.3",
57
- "@nuxt/schema": "4.2.2",
55
+ "@nuxt/schema": "4.4.6",
58
56
  "@types/three": "0.182.0",
59
- "@typescript/native-preview": "7.0.0-dev.20260111.1",
60
- "@vitest/coverage-v8": "4.0.18",
61
- "nuxt": "4.2.2",
57
+ "@typescript/native-preview": "7.0.0-dev.20260205.1",
58
+ "@vitest/coverage-v8": "4.1.5",
59
+ "nuxt": "4.4.6",
62
60
  "typescript": "5.9.3",
63
61
  "unbuild": "2.0.0",
64
- "vitest": "4.0.18",
65
- "vue-router": "4.6.4",
66
- "vue-tsc": "3.2.2",
62
+ "vitest": "4.1.6",
63
+ "vue-router": "5.0.7",
64
+ "vue-tsc": "3.2.9",
67
65
  "tsconfig": "0.0.0"
68
66
  },
69
67
  "scripts": {
@@ -1,23 +0,0 @@
1
- import initUnocssRuntime from "@unocss/runtime";
2
- import { presetWind3 } from "unocss";
3
- import { defineNuxtPlugin, useAppConfig } from "#imports";
4
- import unoConfig from "../../uno.config";
5
-
6
- // Resolves UnoCSS utility classes at runtime via DOM MutationObserver.
7
- // Needed when CMS content contains dynamic classes not known at build time,
8
- // since extracting all CMS-used classes from the backend during build is impractical.
9
- // Trade-off: may cause layout shift as styles are applied after hydration.
10
- // Can be disabled via app.config.ts: { unocssRuntime: false }
11
- // When disabled, CMS classes unknown at build time won't be resolved —
12
- // add them to the UnoCSS safelist in uno.config.ts to ensure they are generated.
13
- export default defineNuxtPlugin(() => {
14
- const appConfig = useAppConfig();
15
- if (!appConfig.unocssRuntime) return;
16
-
17
- initUnocssRuntime({
18
- defaults: {
19
- theme: unoConfig.theme,
20
- presets: [presetWind3()],
21
- },
22
- });
23
- });
package/uno.config.ts DELETED
@@ -1,94 +0,0 @@
1
- import {
2
- defineConfig,
3
- presetAttributify,
4
- presetIcons,
5
- presetTypography,
6
- presetWind3,
7
- } from "unocss";
8
-
9
- export default defineConfig({
10
- shortcuts: {},
11
- preflights: [
12
- {
13
- getCSS: () => `
14
- /* Filter collapse transition */
15
- .filter-collapse-enter-active,
16
- .filter-collapse-leave-active {
17
- transition: all 0.3s ease-in-out;
18
- overflow: hidden;
19
- }
20
-
21
- .filter-collapse-enter-from,
22
- .filter-collapse-leave-to {
23
- max-height: 0;
24
- opacity: 0;
25
- }
26
-
27
- .filter-collapse-enter-to,
28
- .filter-collapse-leave-from {
29
- max-height: 1000px;
30
- opacity: 1;
31
- }
32
- `,
33
- },
34
- ],
35
- theme: {
36
- colors: {
37
- "brand-primary": "#543B95",
38
- "surface-surface": "#FFFFFF",
39
- "outline-outline": "#79747E",
40
- "outline-outline-variant": "#CAC4D0",
41
- "outline-outline-primary": "#543B95",
42
- "surface-on-surface": "#1D1B20",
43
- "surface-surface-variant": "#FBF6FF",
44
- "states-success": "#15B31C",
45
- "surface-on-surface-variant": "#696470",
46
- "surface-surface-disabled": "#E8E8E8",
47
- "surface-on-surface-disabled": "#9893A6",
48
- "surface-surface-primary": "#D0BCFF",
49
- "states-warning": "#F57C00",
50
- "surface-surface-container": "#F3EDF7",
51
- "surface-surface-container-highest": "#E6E0E9",
52
- "states-error": "#D12D24",
53
- "states-on-error": "#FFFFFF",
54
- "brand-primary-hover": "#45317A",
55
- "brand-on-primary": "#FFFFFF",
56
- "brand-secondary": "#E1D5FF",
57
- "brand-secondary-hover": "#D0BCFC",
58
- "brand-on-secondary": "#3A276A",
59
- "brand-tertiary": "#F1F1F1",
60
- "brand-tertiary-hover": "#E3E3E3",
61
- "brand-on-tertiary": "#1D1B20",
62
- "other-sale": "#D12D24",
63
- "overlay-dark-highest": "rgba(0, 0, 0, 0.75)",
64
- "overlay-dark-high": "rgba(0, 0, 0, 0.5)",
65
- "overlay-dark": "rgba(0, 0, 0, 0.3)",
66
- "overlay-dark-low": "rgba(0, 0, 0, 0.12)",
67
- "overlay-dark-lowest": "rgba(0, 0, 0, 0.08)",
68
- "overlay-light-highest": "rgba(255, 255, 255, 0.75)",
69
- "overlay-light-high": "rgba(255, 255, 255, 0.5)",
70
- "overlay-light": "rgba(255, 255, 255, 0.25)",
71
- "overlay-light-low": "rgba(255, 255, 255, 0.12)",
72
- "overlay-light-lowest": "rgba(255, 255, 255, 0.08)",
73
- "fixed-fixed-on-image": "#FFFFFF",
74
- },
75
- fontFamily: {
76
- inter: "Inter",
77
- Noto_Serif: "Noto Serif",
78
- },
79
- },
80
- safelist: ["states-success", "states-error", "states-info", "states-warning"],
81
- presets: [
82
- presetWind3(),
83
- presetIcons({
84
- collections: {
85
- carbon: () =>
86
- import("@iconify-json/carbon/icons.json", {
87
- with: { type: "json" },
88
- }).then((i) => i.default),
89
- },
90
- }),
91
- presetAttributify(),
92
- presetTypography(),
93
- ],
94
- });