@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 +72 -98
- package/app/app.config.ts +1 -0
- package/app/components/SwMedia3D.vue +12 -5
- package/app/components/SwProductCard.vue +4 -0
- package/app/components/SwProductCardImage.vue +28 -30
- package/app/components/SwProductListingFilters.vue +2 -2
- package/app/components/SwProductListingFiltersHorizontal.vue +2 -2
- package/app/components/SwVariantConfigurator.vue +1 -2
- package/app/components/public/cms/CmsGenericBlock.vue +9 -4
- package/app/components/public/cms/CmsGenericElement.vue +7 -2
- package/app/components/public/cms/CmsNoComponent.vue +87 -8
- package/app/components/public/cms/CmsPage.vue +3 -6
- package/app/components/public/cms/FrontendAccountCustomerGroupRegistrationPage.vue +52 -0
- package/app/components/public/cms/element/CmsElementImage.vue +13 -34
- package/app/components/public/cms/element/CmsElementProductListing.vue +6 -2
- package/app/composables/useImagePlaceholder.ts +2 -2
- package/app/composables/useLcpImagePreload.ts +10 -6
- package/app/composables/useTypedAppConfig.ts +15 -0
- package/index.d.ts +37 -29
- package/nuxt.config.ts +5 -0
- package/package.json +16 -18
- 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
|
|
@@ -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
|
|
118
|
+
import baseConfig from './.nuxt/uno.config.mjs'
|
|
119
119
|
|
|
120
|
-
export default mergeConfigs([
|
|
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
|
-
|
|
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
|
-
|
|
377
|
+
### Product Card Images (`SwProductCardImage`)
|
|
386
378
|
|
|
387
|
-
|
|
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
|
-
|
|
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
|
-
});
|
|
382
|
+
// nuxt.config.ts
|
|
383
|
+
productCard: {
|
|
384
|
+
modifiers: { format: "webp", quality: 90, fit: "cover" },
|
|
385
|
+
}
|
|
401
386
|
```
|
|
402
387
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
402
|
+
### CMS Images (`CmsElementImage`)
|
|
421
403
|
|
|
422
|
-
|
|
404
|
+
CMS image elements use `useElementSize()` to measure the rendered container and pass the size to `<NuxtImg>` via `width`/`height` props:
|
|
423
405
|
|
|
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
|
-
```
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
498
|
-
|
|
499
|
-
### Minor Changes
|
|
477
|
+
### Latest changes: 3.0.0
|
|
500
478
|
|
|
501
|
-
|
|
502
|
-
- Extend theme with overlay and fixed color tokens
|
|
479
|
+
### Major Changes
|
|
503
480
|
|
|
504
|
-
- [#
|
|
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
|
-
|
|
483
|
+
### Minor Changes
|
|
507
484
|
|
|
508
|
-
- [#
|
|
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
|
-
|
|
487
|
+
### Patch Changes
|
|
511
488
|
|
|
512
|
-
- [#
|
|
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
|
-
- [#
|
|
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
|
-
- [#
|
|
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
|
-
|
|
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
|
-
- [#
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
- [#
|
|
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
|
-
- [#
|
|
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 [[`
|
|
533
|
-
- @shopware/
|
|
534
|
-
- @shopware/composables@1.11.
|
|
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
|
@@ -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
|
-
<
|
|
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 {
|
|
9
|
-
import {
|
|
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
|
-
|
|
41
|
-
props.product?.cover?.media
|
|
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
|
|
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
|
|
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
|
-
:
|
|
72
|
-
|
|
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"
|
|
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="
|
|
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 {
|
|
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 {
|
|
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 "
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 =
|
|
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,
|
|
13
|
-
import {
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
:
|
|
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="
|
|
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 {
|
|
7
|
-
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
15
|
+
const appConfig = useTypedAppConfig();
|
|
16
|
+
const isEnabled = Boolean(appConfig.lcpImagePreload);
|
|
15
17
|
|
|
16
18
|
const lcpImageHref = computed(() =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shopware/cms-base-layer",
|
|
3
|
-
"version": "
|
|
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.
|
|
38
|
-
"@tresjs/cientos": "5.
|
|
39
|
-
"@tresjs/nuxt": "^5.
|
|
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.
|
|
48
|
-
"
|
|
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.
|
|
53
|
-
"@shopware/helpers": "1.7.
|
|
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.
|
|
55
|
+
"@nuxt/schema": "4.4.6",
|
|
58
56
|
"@types/three": "0.182.0",
|
|
59
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
60
|
-
"@vitest/coverage-v8": "4.
|
|
61
|
-
"nuxt": "4.
|
|
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.
|
|
65
|
-
"vue-router": "
|
|
66
|
-
"vue-tsc": "3.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
|
-
});
|