@shopware/cms-base-layer 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@
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).
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://developer.shopware.com/frontends/concepts/styling.html).
9
9
 
10
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
 
@@ -90,7 +90,7 @@ Since all CMS components are registered in your Nuxt application, you can now st
90
90
 
91
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
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.
93
+ See a [short guide](https://developer.shopware.com/frontends/guides/cms/content-pages.html#use-the-cms-base-package) on how to use `cms-base-layer` in your Nuxt project.
94
94
 
95
95
  ## Styling and Design Tokens
96
96
 
@@ -442,13 +442,13 @@ export default defineAppConfig({
442
442
 
443
443
  ## 📘 Available components
444
444
 
445
- The list of available blocks and elements is [here](https://frontends.shopware.com/packages/cms-base-layer.html#available-components).
445
+ The list of available blocks and elements is [here](https://developer.shopware.com/frontends/packages/cms-base-layer.html#available-components).
446
446
 
447
447
  ## 🔄 Overwriting components
448
448
 
449
449
  The procedure is:
450
450
 
451
- - find a component in component's [list](https://frontends.shopware.com/packages/cms-base.html#available-components), using a [Vue devtools](https://devtools.vuejs.org/) or browsing the github [repository](https://github.com/shopware/frontends/tree/main/packages/cms-base-layer/app/components)
451
+ - find a component in component's [list](https://developer.shopware.com/frontends/packages/cms-base.html#available-components), using a [Vue devtools](https://devtools.vuejs.org/) or browsing the github [repository](https://github.com/shopware/frontends/tree/main/packages/cms-base-layer/app/components)
452
452
  - take its name
453
453
  - create a file with the same name and place it into `~/components` dir in your nuxt project (or wherever according your nuxt config)
454
454
 
@@ -475,7 +475,7 @@ No additional packages needed to be installed.
475
475
 
476
476
  ## Links
477
477
 
478
- - [📘 Documentation](https://frontends.shopware.com)
478
+ - [📘 Documentation](https://developer.shopware.com/frontends)
479
479
 
480
480
  - [👥 Community](https://discord.com/channels/1308047705309708348/1405501315160739951) (`#composable-frontend`)
481
481
 
@@ -485,9 +485,31 @@ No additional packages needed to be installed.
485
485
 
486
486
  Full changelog for stable version is available [here](https://github.com/shopware/frontends/blob/main/packages/cms-base-layer/CHANGELOG.md)
487
487
 
488
- ### Latest changes: 3.0.1
488
+ ### Latest changes: 3.1.0
489
+
490
+ ### Minor Changes
491
+
492
+ - [#2503](https://github.com/shopware/frontends/pull/2503) [`1b0ccc9`](https://github.com/shopware/frontends/commit/1b0ccc95fcef89de42931155fb40119b9075d21b) Thanks [@mkucmus](https://github.com/mkucmus)! - `SwProductListingFilters` and `SwProductListingFiltersHorizontal`: honor the `listingType` prop. When set to `productSearchListing` the components drive `useProductSearchListing` instead of `useCategoryListing` and keep the active search term in the request criteria and resulting URL, so the shared filter sidebar can be reused on the product search page without resetting the query. Category listing behavior is unchanged.
489
493
 
490
494
  ### Patch Changes
491
495
 
492
- - Updated dependencies [[`8be060d`](https://github.com/shopware/frontends/commit/8be060de825ca799f98a8f045a5e7fea61f5d1a2), [`5678fb0`](https://github.com/shopware/frontends/commit/5678fb008cbd86eaddd061e004de89e6f45bb7ec), [`22611e5`](https://github.com/shopware/frontends/commit/22611e542b8f42a4f34dce5186f628f9a17f457b)]:
493
- - @shopware/composables@1.12.0
496
+ - [#2593](https://github.com/shopware/frontends/pull/2593) [`760635c`](https://github.com/shopware/frontends/commit/760635cdbaf3c80ae99fbe0f42a31a8ebaa14675) Thanks [@patzick](https://github.com/patzick)! - Stop registering the `@unocss/nuxt` module from the CMS layer. UnoCSS is not a dependency of this package anymore (it lives in `@shopware/unocss-design-tokens-layer`), so registering the module made every app that doesn't install UnoCSS itself fail to build with `Could not load @unocss/nuxt. Is it installed?` - for example the blank template. Apps that want the shared UnoCSS setup keep getting it from `@shopware/unocss-design-tokens-layer` or their own `@unocss/nuxt` registration.
497
+
498
+ - [#2568](https://github.com/shopware/frontends/pull/2568) [`6315350`](https://github.com/shopware/frontends/commit/6315350add0464abef153343897d42f5808f2003) Thanks [@patzick](https://github.com/patzick)! - Add cacheable GET support for category details and product reviews while preserving the existing POST behavior when `cacheableReads` is disabled.
499
+
500
+ - `useCategorySearch.search` now calls `GET /category/{navigationId}` and sends the complete encoded Criteria in the `_criteria` query parameter when cacheable reads are enabled.
501
+ - `useProductReviews.loadProductReviews` now calls `GET /product/{productId}/reviews` and sends its encoded Criteria in `_criteria` when cacheable reads are enabled.
502
+ - The CMS product description reviews element follows the same flag and endpoint behavior when it needs to fetch reviews directly.
503
+
504
+ - [#2571](https://github.com/shopware/frontends/pull/2571) [`c64ae1c`](https://github.com/shopware/frontends/commit/c64ae1c6e2d11e33bd52df4b55177cc26b646433) Thanks [@patzick](https://github.com/patzick)! - Show the category a shopper came from in product breadcrumbs, while direct product links continue to use the default category.
505
+
506
+ - [#2596](https://github.com/shopware/frontends/pull/2596) [`6572aa8`](https://github.com/shopware/frontends/commit/6572aa84431e1f4a34d6cf04e549037692d638a6) Thanks [@patzick](https://github.com/patzick)! - Reduce Rolldown/Vite build noise: scope Nuxt global components, keep the three.js async chunk warning intentional, avoid shipping full carbon icon JSON via UnoCSS runtime, and bump @vueuse to 14.4.0.
507
+
508
+ - [#2592](https://github.com/shopware/frontends/pull/2592) [`fcc3d71`](https://github.com/shopware/frontends/commit/fcc3d71af05c2f2d409aa63ecb562714e6bc0e69) Thanks [@patzick](https://github.com/patzick)! - Replace CMS layer `@apply` styles with plain CSS declarations to avoid Nuxt build warnings.
509
+
510
+ - [#2568](https://github.com/shopware/frontends/pull/2568) [`6315350`](https://github.com/shopware/frontends/commit/6315350add0464abef153343897d42f5808f2003) Thanks [@patzick](https://github.com/patzick)! - Preserve CMS image aspect ratios when responsive dimensions are calculated after hydration, preventing visible image scaling as optimized sources load.
511
+
512
+ - Updated dependencies [[`6315350`](https://github.com/shopware/frontends/commit/6315350add0464abef153343897d42f5808f2003), [`b767721`](https://github.com/shopware/frontends/commit/b767721847bf3391f9067eca7a045089fb22fce0), [`f16c5a0`](https://github.com/shopware/frontends/commit/f16c5a0785d6187b73c3edcf37feab7c90bd7988), [`978b02c`](https://github.com/shopware/frontends/commit/978b02c969ca4b16f5fc1d7a953ec4cce3d98173), [`6572aa8`](https://github.com/shopware/frontends/commit/6572aa84431e1f4a34d6cf04e549037692d638a6), [`744833b`](https://github.com/shopware/frontends/commit/744833b9d7d2f8ea1f5dfe65be3fa554dbe4a09f), [`e03c91b`](https://github.com/shopware/frontends/commit/e03c91be172374894d90b7a0111855b76719fee1), [`33facb1`](https://github.com/shopware/frontends/commit/33facb178792c8cb26b47ab984ac48c08ab4b72b), [`9137475`](https://github.com/shopware/frontends/commit/91374753cedb2034385f642e6af11314f2971caa), [`474d3fe`](https://github.com/shopware/frontends/commit/474d3fed346816135b0c7c797990b215a8b691c0), [`38379a5`](https://github.com/shopware/frontends/commit/38379a5d52ab09008774533ef417ebe2cde3f7fd)]:
513
+ - @shopware/composables@1.12.1
514
+ - @shopware/api-client@1.5.1
515
+ - @shopware/helpers@1.7.2
@@ -17,6 +17,7 @@ import {
17
17
  useAddToCart,
18
18
  useCartErrorParamsResolver,
19
19
  useCartNotification,
20
+ useNavigationContext,
20
21
  useNotifications,
21
22
  useProductWishlist,
22
23
  useUrlResolver,
@@ -144,10 +145,25 @@ const productManufacturer = computed(() =>
144
145
  getProductManufacturerName(product.value),
145
146
  );
146
147
 
147
- const { getUrlPrefix } = useUrlResolver();
148
- const productLink = computed(() =>
149
- buildUrlPrefix(getProductRoute(product.value), getUrlPrefix()),
148
+ const { foreignKey: currentNavigationId, routeName } = useNavigationContext();
149
+ const referrerCategoryId = computed(() =>
150
+ isProductListing && routeName.value === "frontend.navigation.page"
151
+ ? currentNavigationId.value
152
+ : undefined,
150
153
  );
154
+ const { getUrlPrefix } = useUrlResolver();
155
+ const productLink = computed(() => {
156
+ const productRoute = getProductRoute(product.value);
157
+ const productRouteWithReferrer = {
158
+ ...productRoute,
159
+ state: {
160
+ ...productRoute.state,
161
+ referrerCategoryId: referrerCategoryId.value,
162
+ },
163
+ };
164
+
165
+ return buildUrlPrefix(productRouteWithReferrer, getUrlPrefix());
166
+ });
151
167
  </script>
152
168
 
153
169
  <template>
@@ -5,11 +5,19 @@ import type {
5
5
  } from "@shopware/composables";
6
6
  import { useCmsTranslations } from "@shopware/composables";
7
7
  import { defu } from "defu";
8
- import { computed, reactive } from "vue";
9
- import type { ComputedRef, UnwrapNestedRefs } from "vue";
8
+ import { computed } from "vue";
9
+ import type { ComputedRef } from "vue";
10
10
  import type { LocationQueryRaw } from "vue-router";
11
11
 
12
- import { useCategoryListing, useRoute, useRouter } from "#imports";
12
+ import {
13
+ firstQueryValue,
14
+ toNumber,
15
+ useCategoryListing,
16
+ useProductSearchListing,
17
+ useRoute,
18
+ useRouter,
19
+ useSelectedListingFilters,
20
+ } from "#imports";
13
21
  import type { Schemas, operations } from "#shopware";
14
22
 
15
23
  const props = defineProps<{
@@ -17,6 +25,8 @@ const props = defineProps<{
17
25
  listingType?: string;
18
26
  }>();
19
27
 
28
+ const isProductSearch = props.listingType === "productSearchListing";
29
+
20
30
  type Translations = {
21
31
  listing: {
22
32
  filters: string;
@@ -25,15 +35,6 @@ type Translations = {
25
35
  };
26
36
  };
27
37
 
28
- type FilterState = {
29
- manufacturer: Set<string>;
30
- properties: Set<string>;
31
- "min-price": number | undefined;
32
- "max-price": number | undefined;
33
- rating: number | undefined;
34
- "shipping-free": boolean | undefined;
35
- };
36
-
37
38
  let translations: Translations = {
38
39
  listing: {
39
40
  filters: "Filters",
@@ -54,17 +55,9 @@ const {
54
55
  getInitialFilters,
55
56
  getSortingOrders,
56
57
  search,
57
- } = useCategoryListing();
58
+ } = isProductSearch ? useProductSearchListing() : useCategoryListing();
58
59
 
59
- const sidebarSelectedFilters: UnwrapNestedRefs<FilterState> =
60
- reactive<FilterState>({
61
- manufacturer: new Set(),
62
- properties: new Set(),
63
- "min-price": undefined,
64
- "max-price": undefined,
65
- rating: undefined,
66
- "shipping-free": undefined,
67
- });
60
+ const sidebarSelectedFilters = useSelectedListingFilters();
68
61
 
69
62
  const showResetFiltersButton = computed<boolean>(() => {
70
63
  if (
@@ -94,51 +87,10 @@ const searchCriteriaForRequest: ComputedRef<Schemas["ProductListingCriteria"]> =
94
87
  order: getCurrentSortingOrder.value as string,
95
88
  "shipping-free": sidebarSelectedFilters["shipping-free"] as boolean,
96
89
  rating: sidebarSelectedFilters.rating as number,
97
- search: "",
98
- limit: route.query.limit ? Number(route.query.limit) : 15,
90
+ search: isProductSearch ? (firstQueryValue(route.query.search) ?? "") : "",
91
+ limit: toNumber(firstQueryValue(route.query.limit)) ?? 15,
99
92
  }));
100
93
 
101
- for (const param in route.query) {
102
- if (param in sidebarSelectedFilters) {
103
- const queryValue = route.query[param];
104
-
105
- // Skip arrays
106
- if (Array.isArray(queryValue)) continue;
107
-
108
- if (["manufacturer", "properties"].includes(param)) {
109
- if (typeof queryValue === "string") {
110
- const elements = queryValue.split("|");
111
- const targetSet = sidebarSelectedFilters[
112
- param as keyof FilterState
113
- ] as Set<string>;
114
- for (const element of elements) {
115
- targetSet.add(element);
116
- }
117
- }
118
- } else if (queryValue && typeof queryValue === "string") {
119
- // Fix: Use specific property assignments instead of generic keyof
120
- if (param === "min-price") {
121
- const numValue = Number(queryValue);
122
- if (!Number.isNaN(numValue)) {
123
- sidebarSelectedFilters["min-price"] = numValue;
124
- }
125
- } else if (param === "max-price") {
126
- const numValue = Number(queryValue);
127
- if (!Number.isNaN(numValue)) {
128
- sidebarSelectedFilters["max-price"] = numValue;
129
- }
130
- } else if (param === "rating") {
131
- const numValue = Number(queryValue);
132
- if (!Number.isNaN(numValue)) {
133
- sidebarSelectedFilters.rating = numValue;
134
- }
135
- } else if (param === "shipping-free") {
136
- sidebarSelectedFilters["shipping-free"] = queryValue === "true";
137
- }
138
- }
139
- }
140
- }
141
-
142
94
  const handleFilterChange = async (event: {
143
95
  code: string;
144
96
  value: string | number | boolean;
@@ -172,7 +124,12 @@ const handleFilterChange = async (event: {
172
124
 
173
125
  const executeSearch = async () => {
174
126
  try {
175
- await search(searchCriteriaForRequest.value);
127
+ // Search context refetches from the URL (the page's useAsyncData watches
128
+ // route.query), so navigating below is enough; calling search() here too
129
+ // would double-fetch and flicker. Category listings have no URL watcher.
130
+ if (!isProductSearch) {
131
+ await search(searchCriteriaForRequest.value);
132
+ }
176
133
 
177
134
  // Build query directly from searchCriteriaForRequest which already has pipe-separated strings
178
135
  const criteria = searchCriteriaForRequest.value;
@@ -186,6 +143,12 @@ const executeSearch = async () => {
186
143
  if (criteria["shipping-free"])
187
144
  query["shipping-free"] = criteria["shipping-free"];
188
145
  if (criteria.order) query.order = criteria.order;
146
+ // Keep the search term in the URL so filtering on the search page does not reset the query
147
+ if (isProductSearch && route.query.search)
148
+ query.search = route.query.search;
149
+ // Preserve the page-size selection; rebuilding the query from scratch would
150
+ // otherwise drop `limit` and revert to the default on refresh/navigation.
151
+ if (route.query.limit) query.limit = route.query.limit;
189
152
 
190
153
  await router.push({
191
154
  query: query as LocationQueryRaw,
@@ -215,10 +178,13 @@ const currentSortingOrder = computed({
215
178
  },
216
179
  });
217
180
 
218
- await changeCurrentSortingOrder(order, {
219
- ...(route.query as unknown as operations["searchPage post /search"]["body"]),
220
- limit: route.query.limit ? Number(route.query.limit) : 15,
221
- });
181
+ // Search context refetches from the URL; category fetches directly.
182
+ if (!isProductSearch) {
183
+ await changeCurrentSortingOrder(order, {
184
+ ...(route.query as unknown as operations["searchPage post /search"]["body"]),
185
+ limit: toNumber(firstQueryValue(route.query.limit)) ?? 15,
186
+ });
187
+ }
222
188
  } catch (error) {
223
189
  console.error("Sorting order change failed:", error);
224
190
  }
@@ -5,11 +5,19 @@ import type {
5
5
  } from "@shopware/composables";
6
6
  import { useCmsTranslations } from "@shopware/composables";
7
7
  import { defu } from "defu";
8
- import { computed, reactive } from "vue";
9
- import type { ComputedRef, UnwrapNestedRefs } from "vue";
8
+ import { computed } from "vue";
9
+ import type { ComputedRef } from "vue";
10
10
  import type { LocationQueryRaw } from "vue-router";
11
11
 
12
- import { useCategoryListing, useRoute, useRouter } from "#imports";
12
+ import {
13
+ firstQueryValue,
14
+ toNumber,
15
+ useCategoryListing,
16
+ useProductSearchListing,
17
+ useRoute,
18
+ useRouter,
19
+ useSelectedListingFilters,
20
+ } from "#imports";
13
21
  import type { Schemas, operations } from "#shopware";
14
22
 
15
23
  const props = defineProps<{
@@ -17,6 +25,8 @@ const props = defineProps<{
17
25
  listingType?: string;
18
26
  }>();
19
27
 
28
+ const isProductSearch = props.listingType === "productSearchListing";
29
+
20
30
  type Translations = {
21
31
  listing: {
22
32
  filters: string;
@@ -25,15 +35,6 @@ type Translations = {
25
35
  };
26
36
  };
27
37
 
28
- type FilterState = {
29
- manufacturer: Set<string>;
30
- properties: Set<string>;
31
- "min-price": number | undefined;
32
- "max-price": number | undefined;
33
- rating: number | undefined;
34
- "shipping-free": boolean | undefined;
35
- };
36
-
37
38
  let translations: Translations = {
38
39
  listing: {
39
40
  filters: "Filters",
@@ -53,17 +54,9 @@ const {
53
54
  getInitialFilters,
54
55
  getSortingOrders,
55
56
  search,
56
- } = useCategoryListing();
57
+ } = isProductSearch ? useProductSearchListing() : useCategoryListing();
57
58
 
58
- const sidebarSelectedFilters: UnwrapNestedRefs<FilterState> =
59
- reactive<FilterState>({
60
- manufacturer: new Set(),
61
- properties: new Set(),
62
- "min-price": undefined,
63
- "max-price": undefined,
64
- rating: undefined,
65
- "shipping-free": undefined,
66
- });
59
+ const sidebarSelectedFilters = useSelectedListingFilters();
67
60
 
68
61
  const showResetFiltersButton = computed<boolean>(() => {
69
62
  if (
@@ -93,51 +86,10 @@ const searchCriteriaForRequest: ComputedRef<Schemas["ProductListingCriteria"]> =
93
86
  order: getCurrentSortingOrder.value as string,
94
87
  "shipping-free": sidebarSelectedFilters["shipping-free"] as boolean,
95
88
  rating: sidebarSelectedFilters.rating as number,
96
- search: "",
97
- limit: route.query.limit ? Number(route.query.limit) : 15,
89
+ search: isProductSearch ? (firstQueryValue(route.query.search) ?? "") : "",
90
+ limit: toNumber(firstQueryValue(route.query.limit)) ?? 15,
98
91
  }));
99
92
 
100
- for (const param in route.query) {
101
- if (param in sidebarSelectedFilters) {
102
- const queryValue = route.query[param];
103
-
104
- // Skip arrays
105
- if (Array.isArray(queryValue)) continue;
106
-
107
- if (["manufacturer", "properties"].includes(param)) {
108
- if (typeof queryValue === "string") {
109
- const elements = queryValue.split("|");
110
- const targetSet = sidebarSelectedFilters[
111
- param as keyof FilterState
112
- ] as Set<string>;
113
- for (const element of elements) {
114
- targetSet.add(element);
115
- }
116
- }
117
- } else if (queryValue && typeof queryValue === "string") {
118
- // Fix: Use specific property assignments instead of generic keyof
119
- if (param === "min-price") {
120
- const numValue = Number(queryValue);
121
- if (!Number.isNaN(numValue)) {
122
- sidebarSelectedFilters["min-price"] = numValue;
123
- }
124
- } else if (param === "max-price") {
125
- const numValue = Number(queryValue);
126
- if (!Number.isNaN(numValue)) {
127
- sidebarSelectedFilters["max-price"] = numValue;
128
- }
129
- } else if (param === "rating") {
130
- const numValue = Number(queryValue);
131
- if (!Number.isNaN(numValue)) {
132
- sidebarSelectedFilters.rating = numValue;
133
- }
134
- } else if (param === "shipping-free") {
135
- sidebarSelectedFilters["shipping-free"] = queryValue === "true";
136
- }
137
- }
138
- }
139
- }
140
-
141
93
  const handleFilterChange = async (event: {
142
94
  code: string;
143
95
  value: string | number | boolean;
@@ -171,7 +123,12 @@ const handleFilterChange = async (event: {
171
123
 
172
124
  const executeSearch = async () => {
173
125
  try {
174
- await search(searchCriteriaForRequest.value);
126
+ // Search context refetches from the URL (the page's useAsyncData watches
127
+ // route.query), so navigating below is enough; calling search() here too
128
+ // would double-fetch and flicker. Category listings have no URL watcher.
129
+ if (!isProductSearch) {
130
+ await search(searchCriteriaForRequest.value);
131
+ }
175
132
 
176
133
  // Build query directly from searchCriteriaForRequest which already has pipe-separated strings
177
134
  const criteria = searchCriteriaForRequest.value;
@@ -185,6 +142,12 @@ const executeSearch = async () => {
185
142
  if (criteria["shipping-free"])
186
143
  query["shipping-free"] = criteria["shipping-free"];
187
144
  if (criteria.order) query.order = criteria.order;
145
+ // Keep the search term in the URL so filtering on the search page does not reset the query
146
+ if (isProductSearch && route.query.search)
147
+ query.search = route.query.search;
148
+ // Preserve the page-size selection; rebuilding the query from scratch would
149
+ // otherwise drop `limit` and revert to the default on refresh/navigation.
150
+ if (route.query.limit) query.limit = route.query.limit;
188
151
 
189
152
  await router.push({
190
153
  query: query as LocationQueryRaw,
@@ -214,10 +177,13 @@ const currentSortingOrder = computed({
214
177
  },
215
178
  });
216
179
 
217
- await changeCurrentSortingOrder(order, {
218
- ...(route.query as unknown as operations["searchPage post /search"]["body"]),
219
- limit: route.query.limit ? Number(route.query.limit) : 15,
220
- });
180
+ // Search context refetches from the URL; category fetches directly.
181
+ if (!isProductSearch) {
182
+ await changeCurrentSortingOrder(order, {
183
+ ...(route.query as unknown as operations["searchPage post /search"]["body"]),
184
+ limit: toNumber(firstQueryValue(route.query.limit)) ?? 15,
185
+ });
186
+ }
221
187
  } catch (error) {
222
188
  console.error("Sorting order change failed:", error);
223
189
  }
@@ -71,7 +71,7 @@ const DynamicRender = () => {
71
71
  }
72
72
  if (import.meta.dev) {
73
73
  console.warn(
74
- `[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`,
74
+ `[CMS] Block type "${componentName}" is not implemented.\n → Create a component named "${componentNameToResolve}.vue" to render it.\n 📖 Docs: https://developer.shopware.com/frontends/guides/cms/create-blocks`,
75
75
  );
76
76
  return h(resolveComponent("CmsNoComponent"), { content: props.content });
77
77
  }
@@ -31,7 +31,7 @@ const DynamicRender = () => {
31
31
  }
32
32
  if (import.meta.dev) {
33
33
  console.warn(
34
- `[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
+ `[CMS] Element type "${componentName}" is not implemented.\n → Create a component named "${componentNameToResolve}.vue" to render it.\n 📖 Docs: https://developer.shopware.com/frontends/guides/cms/create-elements`,
35
35
  );
36
36
  return h(resolveComponent("CmsNoComponent"), { content: props.content });
37
37
  }
@@ -28,7 +28,7 @@ const docsUrl = computed(() => {
28
28
  component: expectedComponentName.value,
29
29
  type: elementType.value.toLowerCase(),
30
30
  });
31
- return `https://frontends.shopware.com/getting-started/cms/missing-component?${params}`;
31
+ return `https://developer.shopware.com/frontends/guides/cms/missing-component?${params}`;
32
32
  });
33
33
 
34
34
  const aiPrompt = computed(() => {
@@ -22,6 +22,7 @@ const slotCenterContent = getSlotContent("center");
22
22
 
23
23
  <style scoped>
24
24
  .cms-block-center-text .cms-element-image {
25
- @apply self-stretch min-h-12;
25
+ align-self: stretch;
26
+ min-height: 3rem;
26
27
  }
27
28
  </style>
@@ -30,6 +30,10 @@ const centerContent = getSlotContent("center");
30
30
  </template>
31
31
  <style scoped>
32
32
  .cms-block-image-bubble-row :deep(.cms-element-image) {
33
- @apply object-cover max-w-xs overflow-hidden rounded-full aspect-square;
33
+ aspect-ratio: 1 / 1;
34
+ max-width: 20rem;
35
+ overflow: hidden;
36
+ border-radius: 9999px;
37
+ object-fit: cover;
34
38
  }
35
39
  </style>
@@ -35,10 +35,14 @@ const centerRightContent = getSlotContent("center-right");
35
35
 
36
36
  <style scoped>
37
37
  .cms-block-image-four-column :deep(.cms-element-image) {
38
- @apply relative h-full w-full;
38
+ position: relative;
39
+ height: 100%;
40
+ width: 100%;
39
41
  }
40
42
 
41
43
  .cms-block-image-four-column :deep(.cms-element-image img) {
42
- @apply w-full h-full object-cover;
44
+ height: 100%;
45
+ width: 100%;
46
+ object-fit: cover;
43
47
  }
44
48
  </style>
@@ -20,24 +20,28 @@ const cmsContent = getSlotContent("imageGallery");
20
20
 
21
21
  <style scoped>
22
22
  .cms-block-image-gallery-big {
23
- @apply w-full;
23
+ width: 100%;
24
24
  }
25
25
 
26
26
  .cms-block-image-gallery-big :deep(.cms-element-image-gallery) {
27
- @apply max-w-screen-xl mx-auto;
27
+ max-width: 1280px;
28
+ margin-left: auto;
29
+ margin-right: auto;
28
30
  }
29
31
 
30
32
  /* Enhanced styling for the big gallery version */
31
33
  .cms-block-image-gallery-big :deep(.gallery-slider) {
32
- @apply max-h-[800px];
34
+ max-height: 800px;
33
35
  }
34
36
 
35
37
  .cms-block-image-gallery-big :deep(img) {
36
- @apply object-contain mx-auto;
38
+ margin-left: auto;
39
+ margin-right: auto;
40
+ object-fit: contain;
37
41
  }
38
42
 
39
43
  /* Larger navigation controls */
40
44
  .cms-block-image-gallery-big :deep(.gallery-slider-controls) {
41
- @apply scale-125;
45
+ transform: scale(1.25);
42
46
  }
43
47
  </style>
@@ -31,10 +31,15 @@ const centerContent = getSlotContent("center");
31
31
 
32
32
  <style scoped>
33
33
  .cms-block-image-highlight-row :deep(.cms-element-image) {
34
- @apply relative h-full w-full border-[12px] border-surface-surface;
34
+ position: relative;
35
+ height: 100%;
36
+ width: 100%;
37
+ border: 12px solid #ffffff;
35
38
  }
36
39
 
37
40
  .cms-block-image-highlight-row :deep(.cms-element-image img) {
38
- @apply w-full h-full object-cover;
41
+ height: 100%;
42
+ width: 100%;
43
+ object-fit: cover;
39
44
  }
40
45
  </style>
@@ -43,14 +43,20 @@ const rightTextContent = getSlotContent("right-text");
43
43
 
44
44
  <style scoped>
45
45
  .cms-block-image-text-row :deep(.cms-element-image) {
46
- @apply relative h-full w-full;
46
+ position: relative;
47
+ height: 100%;
48
+ width: 100%;
47
49
  }
48
50
 
49
51
  .cms-block-image-text-row :deep(.cms-element-image img) {
50
- @apply w-full h-full object-cover rounded-lg;
52
+ height: 100%;
53
+ width: 100%;
54
+ border-radius: 0.5rem;
55
+ object-fit: cover;
51
56
  }
52
57
 
53
58
  .cms-block-image-text-row :deep(.cms-element-text) {
54
- @apply self-stretch min-h-12;
59
+ align-self: stretch;
60
+ min-height: 3rem;
55
61
  }
56
62
  </style>
@@ -31,10 +31,15 @@ const centerContent = getSlotContent("center");
31
31
 
32
32
  <style scoped>
33
33
  .cms-block-image-three-cover :deep(.cms-element-image) {
34
- @apply aspect-square relative h-full w-full;
34
+ position: relative;
35
+ aspect-ratio: 1 / 1;
36
+ height: 100%;
37
+ width: 100%;
35
38
  }
36
39
 
37
40
  .cms-block-image-three-cover :deep(.cms-element-image img) {
38
- @apply w-full h-full object-cover;
41
+ height: 100%;
42
+ width: 100%;
43
+ object-fit: cover;
39
44
  }
40
45
  </style>
@@ -27,14 +27,18 @@ const rightContent = getSlotContent("right");
27
27
 
28
28
  <style scoped>
29
29
  .cms-block-image-two-column :deep(.cms-element-image) {
30
- @apply relative h-full w-full;
30
+ position: relative;
31
+ height: 100%;
32
+ width: 100%;
31
33
  }
32
34
 
33
35
  .cms-block-image-two-column :deep(.cms-element-image img) {
34
- @apply w-full h-full object-cover;
36
+ height: 100%;
37
+ width: 100%;
38
+ object-fit: cover;
35
39
  }
36
40
 
37
41
  .cms-block-image-two-column :deep(.cms-element-image-slider) {
38
- @apply w-full;
42
+ width: 100%;
39
43
  }
40
44
  </style>
@@ -22,6 +22,7 @@ const rightContent = getSlotContent("right");
22
22
 
23
23
  <style scoped>
24
24
  .cms-block-product-heading .cms-element-image {
25
- @apply max-w-[7.5rem] max-h-[7.5rem];
25
+ max-height: 7.5rem;
26
+ max-width: 7.5rem;
26
27
  }
27
28
  </style>
@@ -92,7 +92,6 @@ const SwMedia3D = computed(() => {
92
92
  preset="productDetail"
93
93
  loading="lazy"
94
94
  :width="imageSize"
95
- :height="imageSize"
96
95
  :class="{
97
96
  'w-full': !imageGallery,
98
97
  'h-full': !imageGallery && ['cover', 'stretch'].includes(displayMode),
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { encodeForQuery } from "@shopware/api-client/helpers";
2
3
  import type { CmsElementProductDescriptionReviews } from "@shopware/composables";
3
4
  import { useCmsTranslations } from "@shopware/composables";
4
5
  import { getTranslatedProperty } from "@shopware/helpers";
@@ -56,18 +57,26 @@ const isSectionOpen = (sectionNumber: number) => {
56
57
  };
57
58
 
58
59
  const reviews: Ref<Schemas["ProductReview"][]> = ref([]);
59
- const { apiClient } = useShopwareContext();
60
+ const { apiClient, cacheableReads } = useShopwareContext();
60
61
  const { isLoggedIn } = useUser();
61
62
  const reviewAdded = ref(false);
62
63
 
63
64
  const fetchReviews = async () => {
64
65
  try {
65
- const reviewsResponse = await apiClient.invoke(
66
- "readProductReviews post /product/{productId}/reviews",
67
- {
68
- pathParams: { productId: product.value.id },
69
- },
70
- );
66
+ const reviewsResponse = cacheableReads
67
+ ? await apiClient.invoke(
68
+ "readProductReviewsGet get /product/{productId}/reviews",
69
+ {
70
+ pathParams: { productId: product.value.id },
71
+ query: { _criteria: encodeForQuery({}) },
72
+ },
73
+ )
74
+ : await apiClient.invoke(
75
+ "readProductReviews post /product/{productId}/reviews",
76
+ {
77
+ pathParams: { productId: product.value.id },
78
+ },
79
+ );
71
80
  reviews.value = reviewsResponse.data.elements || [];
72
81
  } catch (error) {
73
82
  console.error("Failed to fetch reviews:", error);
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { firstQueryValue, toNumber } from "./routeQuery";
4
+
5
+ describe("firstQueryValue", () => {
6
+ it("returns a plain string value", () => {
7
+ expect(firstQueryValue("name-asc")).toBe("name-asc");
8
+ });
9
+
10
+ it("returns the first element of a repeated (array) param", () => {
11
+ expect(firstQueryValue(["30", "45"])).toBe("30");
12
+ });
13
+
14
+ it("returns undefined for undefined", () => {
15
+ expect(firstQueryValue(undefined)).toBeUndefined();
16
+ });
17
+
18
+ it("returns undefined for null", () => {
19
+ expect(firstQueryValue(null)).toBeUndefined();
20
+ });
21
+
22
+ it("returns undefined for an empty array", () => {
23
+ expect(firstQueryValue([])).toBeUndefined();
24
+ });
25
+
26
+ it("returns undefined when the first array element is null", () => {
27
+ expect(firstQueryValue([null])).toBeUndefined();
28
+ });
29
+ });
30
+
31
+ describe("toNumber", () => {
32
+ it("parses an integer string", () => {
33
+ expect(toNumber("15")).toBe(15);
34
+ });
35
+
36
+ it("parses a decimal string", () => {
37
+ expect(toNumber("19.99")).toBe(19.99);
38
+ });
39
+
40
+ it("parses zero and negative values", () => {
41
+ expect(toNumber("0")).toBe(0);
42
+ expect(toNumber("-5")).toBe(-5);
43
+ });
44
+
45
+ it("returns undefined for undefined", () => {
46
+ expect(toNumber(undefined)).toBeUndefined();
47
+ });
48
+
49
+ it("returns undefined for an empty string", () => {
50
+ expect(toNumber("")).toBeUndefined();
51
+ });
52
+
53
+ it("returns undefined for a non-numeric string", () => {
54
+ expect(toNumber("abc")).toBeUndefined();
55
+ expect(toNumber("12px")).toBeUndefined();
56
+ });
57
+
58
+ it("returns undefined for non-finite values", () => {
59
+ expect(toNumber("Infinity")).toBeUndefined();
60
+ });
61
+
62
+ it("composes with firstQueryValue for repeated numeric params", () => {
63
+ expect(toNumber(firstQueryValue(["30", "45"]))).toBe(30);
64
+ });
65
+ });
@@ -0,0 +1,26 @@
1
+ import type { LocationQuery } from "vue-router";
2
+
3
+ /**
4
+ * Collapses a route-query param to a single string.
5
+ *
6
+ * Router query values are `string | null`, an array when a param is repeated
7
+ * (e.g. `?p=1&p=2`), or `undefined` when absent. Returns the first usable
8
+ * string, or `undefined` for missing/empty/null values.
9
+ */
10
+ export const firstQueryValue = (
11
+ value: LocationQuery[string] | undefined,
12
+ ): string | undefined => {
13
+ const raw = Array.isArray(value) ? value[0] : value;
14
+ return raw ?? undefined;
15
+ };
16
+
17
+ /**
18
+ * Parses a query string (typically from {@link firstQueryValue}) into a finite
19
+ * number, or `undefined` when missing/empty/non-numeric. Lets callers fall back
20
+ * with `??` instead of forwarding `NaN` into a request.
21
+ */
22
+ export const toNumber = (value: string | undefined): number | undefined => {
23
+ if (value === undefined || value === "") return undefined;
24
+ const parsed = Number(value);
25
+ return Number.isFinite(parsed) ? parsed : undefined;
26
+ };
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ applyQueryToFilters,
5
+ createEmptyFilterState,
6
+ } from "./useSelectedListingFilters";
7
+
8
+ describe("applyQueryToFilters", () => {
9
+ it("populates sets from pipe-joined values", () => {
10
+ const state = createEmptyFilterState();
11
+ applyQueryToFilters(state, { manufacturer: "a|b", properties: "x" });
12
+ expect([...state.manufacturer]).toEqual(["a", "b"]);
13
+ expect([...state.properties]).toEqual(["x"]);
14
+ });
15
+
16
+ it("clears removed filters when the query no longer has them (Back/Forward)", () => {
17
+ const state = createEmptyFilterState();
18
+ applyQueryToFilters(state, { manufacturer: "a|b", search: "shirt" });
19
+ applyQueryToFilters(state, { search: "shirt" }); // manufacturer removed
20
+ expect(state.manufacturer.size).toBe(0);
21
+ expect(state.properties.size).toBe(0);
22
+ });
23
+
24
+ it("clears removed scalar filters on resync", () => {
25
+ const state = createEmptyFilterState();
26
+ applyQueryToFilters(state, {
27
+ "min-price": "10",
28
+ "max-price": "20",
29
+ rating: "4",
30
+ "shipping-free": "true",
31
+ });
32
+ applyQueryToFilters(state, {});
33
+ expect(state["min-price"]).toBeUndefined();
34
+ expect(state["max-price"]).toBeUndefined();
35
+ expect(state.rating).toBeUndefined();
36
+ expect(state["shipping-free"]).toBeUndefined();
37
+ });
38
+
39
+ it("keeps the Set identity stable across re-applies (reactive binding)", () => {
40
+ const state = createEmptyFilterState();
41
+ const manufacturerRef = state.manufacturer;
42
+ const propertiesRef = state.properties;
43
+ applyQueryToFilters(state, { manufacturer: "a" });
44
+ expect(state.manufacturer).toBe(manufacturerRef);
45
+ expect(state.properties).toBe(propertiesRef);
46
+ });
47
+
48
+ it("drops empty members from pipe-joined values", () => {
49
+ const state = createEmptyFilterState();
50
+ applyQueryToFilters(state, { manufacturer: "a||b|" });
51
+ expect([...state.manufacturer]).toEqual(["a", "b"]);
52
+ });
53
+
54
+ it("parses scalars array/NaN-safe and ignores junk", () => {
55
+ const state = createEmptyFilterState();
56
+ applyQueryToFilters(state, {
57
+ "min-price": ["10", "20"],
58
+ "max-price": "abc",
59
+ rating: "4",
60
+ "shipping-free": "true",
61
+ });
62
+ expect(state["min-price"]).toBe(10); // first of repeated param
63
+ expect(state["max-price"]).toBeUndefined(); // NaN -> undefined
64
+ expect(state.rating).toBe(4);
65
+ expect(state["shipping-free"]).toBe(true);
66
+ });
67
+
68
+ it("treats shipping-free other than 'true' as false", () => {
69
+ const state = createEmptyFilterState();
70
+ applyQueryToFilters(state, { "shipping-free": "false" });
71
+ expect(state["shipping-free"]).toBe(false);
72
+ });
73
+
74
+ it("is idempotent (same query twice -> equal state)", () => {
75
+ const state = createEmptyFilterState();
76
+ const query = { manufacturer: "a|b", rating: "4" };
77
+ applyQueryToFilters(state, query);
78
+ applyQueryToFilters(state, query);
79
+ expect([...state.manufacturer]).toEqual(["a", "b"]);
80
+ expect(state.rating).toBe(4);
81
+ });
82
+ });
@@ -0,0 +1,105 @@
1
+ import { reactive, watch } from "vue";
2
+ import type { UnwrapNestedRefs } from "vue";
3
+ import { useRoute } from "vue-router";
4
+ import type { LocationQuery } from "vue-router";
5
+
6
+ import { firstQueryValue, toNumber } from "./routeQuery";
7
+
8
+ export type FilterState = {
9
+ manufacturer: Set<string>;
10
+ properties: Set<string>;
11
+ "min-price": number | undefined;
12
+ "max-price": number | undefined;
13
+ rating: number | undefined;
14
+ "shipping-free": boolean | undefined;
15
+ };
16
+
17
+ /** Fresh, fully-empty filter state. The single source of "no selection". */
18
+ export const createEmptyFilterState = (): FilterState => ({
19
+ manufacturer: new Set<string>(),
20
+ properties: new Set<string>(),
21
+ "min-price": undefined,
22
+ "max-price": undefined,
23
+ rating: undefined,
24
+ "shipping-free": undefined,
25
+ });
26
+
27
+ /**
28
+ * Resets `state` to empty, then repopulates it from `query`. Pure with respect
29
+ * to the router: it only mutates `state`, never navigates or fetches.
30
+ *
31
+ * Reset-first is what clears filters whose query param vanished (e.g. browser
32
+ * Back/Forward), which the old add-only loop left lingering. Idempotent:
33
+ * applying the same query twice yields identical contents, so it is safe to
34
+ * call from a route.query watcher that also fires on the user's own
35
+ * executeSearch() push.
36
+ *
37
+ * Array-valued params (e.g. ?manufacturer=a&manufacturer=b) collapse to the
38
+ * first value via firstQueryValue; numeric params are NaN-safe via toNumber -
39
+ * consistent with the rest of the listing parsing.
40
+ */
41
+ export const applyQueryToFilters = (
42
+ state: FilterState,
43
+ query: LocationQuery,
44
+ ): void => {
45
+ // 1) Reset every field. Clear the Sets in place so their identity is kept
46
+ // (props bound to them stay reactive); replace scalars with undefined.
47
+ state.manufacturer.clear();
48
+ state.properties.clear();
49
+ state["min-price"] = undefined;
50
+ state["max-price"] = undefined;
51
+ state.rating = undefined;
52
+ state["shipping-free"] = undefined;
53
+
54
+ // 2) Repopulate from the current query.
55
+ for (const code of ["manufacturer", "properties"] as const) {
56
+ const value = firstQueryValue(query[code]);
57
+ if (!value) continue;
58
+ for (const element of value.split("|")) {
59
+ if (element) state[code].add(element);
60
+ }
61
+ }
62
+
63
+ const minPrice = toNumber(firstQueryValue(query["min-price"]));
64
+ if (minPrice !== undefined) state["min-price"] = minPrice;
65
+
66
+ const maxPrice = toNumber(firstQueryValue(query["max-price"]));
67
+ if (maxPrice !== undefined) state["max-price"] = maxPrice;
68
+
69
+ const rating = toNumber(firstQueryValue(query.rating));
70
+ if (rating !== undefined) state.rating = rating;
71
+
72
+ const shippingFree = firstQueryValue(query["shipping-free"]);
73
+ if (shippingFree !== undefined) {
74
+ state["shipping-free"] = shippingFree === "true";
75
+ }
76
+ };
77
+
78
+ /**
79
+ * Returns the reactive filter state, populated from the current route.query at
80
+ * setup (runs on server AND client -> identical first render, no hydration
81
+ * mismatch) and kept in sync on every subsequent query-only navigation
82
+ * (Back/Forward, manual URL edits, the component's own executeSearch push).
83
+ *
84
+ * The watcher is intentionally NOT immediate: the setup-time apply already
85
+ * covers the first render, and an immediate watcher would re-run during
86
+ * hydration. It only resyncs UI state - it never navigates or fetches - so it
87
+ * cannot loop with executeSearch() nor cause an extra listing request.
88
+ */
89
+ export const useSelectedListingFilters = (): UnwrapNestedRefs<FilterState> => {
90
+ const route = useRoute();
91
+ const state = reactive<FilterState>(createEmptyFilterState());
92
+
93
+ // Initial parse (SSR + first client render).
94
+ applyQueryToFilters(state as FilterState, route.query);
95
+
96
+ // Resync on query-only route changes while the component stays mounted.
97
+ watch(
98
+ () => route.query,
99
+ (query) => {
100
+ applyQueryToFilters(state as FilterState, query);
101
+ },
102
+ );
103
+
104
+ return state;
105
+ };
package/nuxt.config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createResolver } from "@nuxt/kit";
2
2
  import type { NuxtConfig } from "@nuxt/schema";
3
3
  import { defineNuxtConfig } from "nuxt/config";
4
+ import type { LoggingFunction, RollupLog } from "rollup";
4
5
 
5
6
  const { resolve: resolveLayer } = createResolver(import.meta.url);
6
7
 
@@ -9,7 +10,11 @@ export default defineNuxtConfig({
9
10
  // to prevent bundling heavy 3D libraries in the initial bundle.
10
11
  // If you need 3D support, add "@tresjs/nuxt" to your app's nuxt.config.ts modules array
11
12
  // and dynamically import SwMedia3D using defineAsyncComponent.
12
- modules: ["@unocss/nuxt", "@nuxt/image"],
13
+ //
14
+ // @unocss/nuxt is not included here either. This layer only ships CMS components,
15
+ // the UnoCSS setup lives in @shopware/unocss-design-tokens-layer (or your own config),
16
+ // so apps that don't use UnoCSS can still extend this layer.
17
+ modules: ["@nuxt/image"],
13
18
 
14
19
  hooks: {
15
20
  "components:extend"(components) {
@@ -115,6 +120,24 @@ export default defineNuxtConfig({
115
120
  optimizeDeps: {
116
121
  include: ["xss"],
117
122
  },
123
+ build: {
124
+ // Async SwMedia3D/three chunk is ~977 kB after minify; that split is intentional.
125
+ chunkSizeWarningLimit: 1000,
126
+ },
127
+ },
128
+ nitro: {
129
+ rollupConfig: {
130
+ // @vueuse/shared still embeds @__NO_SIDE_EFFECTS__ inside JSDoc (injectLocal).
131
+ // Nitro/Rollup treats that as a misplaced annotation. Track upstream vueuse.
132
+ onwarn(warning: RollupLog, warn: LoggingFunction) {
133
+ if (
134
+ warning.message?.includes("annotation that Rollup cannot interpret")
135
+ ) {
136
+ return;
137
+ }
138
+ warn(warning);
139
+ },
140
+ },
118
141
  },
119
142
  telemetry: {
120
143
  enabled: false,
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@shopware/cms-base-layer",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "Vue CMS Nuxt Layer for Shopware",
5
5
  "author": "Shopware",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/shopware/frontends.git"
9
9
  },
10
- "homepage": "https://frontends.shopware.com/framework/shopping-experiences.html",
10
+ "homepage": "https://developer.shopware.com/frontends/concepts/shopping-experiences.html",
11
11
  "keywords": [
12
12
  "Shopware",
13
13
  "Vue",
@@ -28,39 +28,38 @@
28
28
  "index.cjs",
29
29
  "index.d.ts",
30
30
  "app.config.ts",
31
- "nuxt.config.ts",
32
- "uno.config.ts"
31
+ "nuxt.config.ts"
33
32
  ],
34
33
  "dependencies": {
35
- "@iconify-json/carbon": "1.2.23",
34
+ "@iconify-json/carbon": "1.2.24",
36
35
  "@nuxt/image": "2.0.0",
37
36
  "@nuxt/kit": "4.4.8",
38
37
  "@tresjs/cientos": "5.7.2",
39
- "@tresjs/nuxt": "^5.6.1",
38
+ "@tresjs/nuxt": "^5.6.3",
40
39
  "@vuelidate/core": "2.0.3",
41
40
  "@vuelidate/validators": "2.0.4",
42
- "@vueuse/core": "14.3.0",
43
- "entities": "6.0.0",
41
+ "@vueuse/core": "14.4.0",
42
+ "entities": "8.0.0",
44
43
  "scule": "1.3.0",
45
44
  "html-to-ast": "0.0.6",
46
45
  "three": "0.183.2",
47
- "vue": "3.5.38",
46
+ "vue": "3.5.39",
48
47
  "xss": "1.0.15",
49
- "@shopware/helpers": "1.7.1",
50
- "@shopware/composables": "1.12.0",
51
- "@shopware/api-client": "1.5.0"
48
+ "@shopware/api-client": "1.5.1",
49
+ "@shopware/composables": "1.12.1",
50
+ "@shopware/helpers": "1.7.2"
52
51
  },
53
52
  "devDependencies": {
54
53
  "@nuxt/schema": "4.4.8",
55
54
  "@types/three": "0.182.0",
56
55
  "@typescript/native-preview": "7.0.0-dev.20260205.1",
57
- "@vitest/coverage-v8": "4.1.9",
56
+ "@vitest/coverage-v8": "4.1.10",
58
57
  "nuxt": "4.4.8",
59
58
  "typescript": "5.9.3",
60
59
  "unbuild": "3.6.1",
61
- "vitest": "4.1.9",
60
+ "vitest": "4.1.10",
62
61
  "vue-router": "5.1.0",
63
- "vue-tsc": "3.3.5",
62
+ "vue-tsc": "3.3.7",
64
63
  "tsconfig": "0.0.0"
65
64
  },
66
65
  "scripts": {