@osimart/create-storefront 0.0.0-stage → 1.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 +80 -2
- package/THEME_DEV_API.md +80 -0
- package/app/app.vue +154 -0
- package/app/assets/css/osi-controls.css +824 -0
- package/app/components/SectionRenderer.vue +46 -0
- package/app/components/builder/BuilderOverlay.vue +582 -0
- package/app/components/checkout/ExtraFees.vue +329 -0
- package/app/components/checkout/PaymentHandoff.vue +388 -0
- package/app/components/common/Banner.vue +35 -0
- package/app/components/common/ConfirmDeleteModal.vue +117 -0
- package/app/components/common/DropdownSelect.vue +432 -0
- package/app/components/common/Loader.vue +32 -0
- package/app/components/common/OsiField.vue +218 -0
- package/app/components/common/PhoneNumInput.vue +434 -0
- package/app/composables/UseBodyScrollLock.js +122 -0
- package/app/composables/UseThemeGroup.js +575 -0
- package/app/composables/useAddToCart.js +87 -0
- package/app/composables/useAnalytics.js +15 -0
- package/app/composables/useAnnouncements.js +29 -0
- package/app/composables/useApiErrorToast.js +104 -0
- package/app/composables/useAuth.js +99 -0
- package/app/composables/useBanners.js +25 -0
- package/app/composables/useBuilderPreview.js +320 -0
- package/app/composables/useButtonStyle.js +107 -0
- package/app/composables/useCarousel.js +160 -0
- package/app/composables/useCart.js +109 -0
- package/app/composables/useCheckout.js +76 -0
- package/app/composables/useClassificationItems.js +269 -0
- package/app/composables/useClassificationPage.js +545 -0
- package/app/composables/useClassificationUrl.js +63 -0
- package/app/composables/useColorScheme.js +44 -0
- package/app/composables/useContactForm.js +53 -0
- package/app/composables/useContentTree.js +770 -0
- package/app/composables/useDragScroll.js +73 -0
- package/app/composables/useFooterSocials.js +107 -0
- package/app/composables/useInputStyle.js +45 -0
- package/app/composables/useLayoutShell.js +114 -0
- package/app/composables/useLazyVideo.js +36 -0
- package/app/composables/useMediaTypes.js +66 -0
- package/app/composables/useMediaUrl.js +71 -0
- package/app/composables/useMenu.js +100 -0
- package/app/composables/useNavStyle.js +219 -0
- package/app/composables/usePlaceholders.js +294 -0
- package/app/composables/usePolicies.js +26 -0
- package/app/composables/useProductCardMedia.js +135 -0
- package/app/composables/useProductCardStyle.js +132 -0
- package/app/composables/useProductDetail.js +982 -0
- package/app/composables/useProductGallery.js +166 -0
- package/app/composables/useProductItems.js +270 -0
- package/app/composables/useProductUrl.js +60 -0
- package/app/composables/useProductVariants.js +210 -0
- package/app/composables/useQuickShop.js +115 -0
- package/app/composables/useRelatedProducts.js +186 -0
- package/app/composables/useRenderContext.js +45 -0
- package/app/composables/useScopedCss.js +106 -0
- package/app/composables/useSearch.js +35 -0
- package/app/composables/useSectionData.js +462 -0
- package/app/composables/useSectionShell.js +113 -0
- package/app/composables/useSidebar.js +69 -0
- package/app/composables/useStoreData.js +57 -0
- package/app/composables/useStoreInit.js +52 -0
- package/app/composables/useStorefront.js +239 -0
- package/app/composables/useT.js +21 -0
- package/app/composables/useTextStyles.js +84 -0
- package/app/composables/useThemeControls.js +34 -0
- package/app/composables/useThemeSeo.js +91 -0
- package/app/composables/useThemeTokens.js +171 -0
- package/app/composables/useWishlist.js +66 -0
- package/app/logic/components/AnnouncementBar.js +118 -0
- package/app/logic/components/CardGrid.js +125 -0
- package/app/logic/components/CardList.js +98 -0
- package/app/logic/components/CartDrawer.js +273 -0
- package/app/logic/components/CartItem.js +31 -0
- package/app/logic/components/ClassificationBento.js +6 -0
- package/app/logic/components/ClassificationCard.js +6 -0
- package/app/logic/components/ClassificationCarousel.js +115 -0
- package/app/logic/components/ClassificationEditorial.js +41 -0
- package/app/logic/components/ClassificationFeatured.js +57 -0
- package/app/logic/components/ClassificationGrid.js +6 -0
- package/app/logic/components/ClassificationNav.js +61 -0
- package/app/logic/components/ClassificationPart.js +72 -0
- package/app/logic/components/FooterBlock.js +91 -0
- package/app/logic/components/FooterSocials.js +60 -0
- package/app/logic/components/HeaderDropdownList.js +29 -0
- package/app/logic/components/HeaderIcon.js +68 -0
- package/app/logic/components/HeaderLogo.js +39 -0
- package/app/logic/components/HeaderNav.js +268 -0
- package/app/logic/components/MobileMenu.js +280 -0
- package/app/logic/components/ProductBuyButtons.js +66 -0
- package/app/logic/components/ProductCard.js +160 -0
- package/app/logic/components/ProductFilters.js +37 -0
- package/app/logic/components/ProductGallery.js +19 -0
- package/app/logic/components/ProductInfoBlock.js +411 -0
- package/app/logic/components/ProductLightbox.js +12 -0
- package/app/logic/components/ProductQuantity.js +31 -0
- package/app/logic/components/ProductVariantPicker.js +125 -0
- package/app/logic/components/QuickBuy.js +194 -0
- package/app/logic/components/SearchOverlay.js +41 -0
- package/app/logic/components/SidebarMenuItem.js +39 -0
- package/app/logic/components/SiteFooter.js +476 -0
- package/app/logic/components/SiteHeader.js +266 -0
- package/app/logic/components/WhatsAppButton.js +75 -0
- package/app/logic/layouts/default.js +66 -0
- package/app/logic/sections/AnnouncementBarSection.js +6 -0
- package/app/logic/sections/BlockCarousel.js +103 -0
- package/app/logic/sections/ClassificationListSection.js +40 -0
- package/app/logic/sections/ContactFormSection.js +294 -0
- package/app/logic/sections/ContactInfoSection.js +419 -0
- package/app/logic/sections/ContentNode.js +189 -0
- package/app/logic/sections/CountdownSection.js +254 -0
- package/app/logic/sections/CustomHtmlSection.js +51 -0
- package/app/logic/sections/DividerSection.js +31 -0
- package/app/logic/sections/FaqSection.js +53 -0
- package/app/logic/sections/FeatureListSection.js +6 -0
- package/app/logic/sections/FeaturedProductSection.js +40 -0
- package/app/logic/sections/FooterSection.js +16 -0
- package/app/logic/sections/FooterUtilitiesSection.js +24 -0
- package/app/logic/sections/GallerySection.js +232 -0
- package/app/logic/sections/HeaderSection.js +24 -0
- package/app/logic/sections/HeroSection.js +24 -0
- package/app/logic/sections/LogoListSection.js +6 -0
- package/app/logic/sections/MainClassificationSection.js +473 -0
- package/app/logic/sections/MainProductSection.js +211 -0
- package/app/logic/sections/MainSearchSection.js +6 -0
- package/app/logic/sections/MapSection.js +92 -0
- package/app/logic/sections/MediaContentSection.js +38 -0
- package/app/logic/sections/MediaWithTextSection.js +238 -0
- package/app/logic/sections/NewsletterSection.js +126 -0
- package/app/logic/sections/ProductHighlightSection.js +201 -0
- package/app/logic/sections/ProductListSection.js +45 -0
- package/app/logic/sections/ProductRecommendationsSection.js +113 -0
- package/app/logic/sections/RichTextSection.js +6 -0
- package/app/logic/sections/ScrollingTextSection.js +69 -0
- package/app/logic/sections/SearchHeaderSection.js +22 -0
- package/app/logic/sections/SlideshowSection.js +118 -0
- package/app/logic/sections/SpacerSection.js +37 -0
- package/app/logic/sections/SplitShowcaseSection.js +6 -0
- package/app/logic/sections/TestimonialListSection.js +6 -0
- package/app/logic/sections/VideoSection.js +235 -0
- package/app/pages/[category]/[product].vue +196 -0
- package/app/pages/__builder-preview.vue +38 -0
- package/app/pages/categories/[slug].vue +208 -0
- package/app/pages/checkout.vue +1972 -0
- package/app/pages/forgot-password.vue +115 -0
- package/app/pages/index.vue +64 -0
- package/app/pages/login.vue +585 -0
- package/app/pages/order-detail.vue +442 -0
- package/app/pages/pages/[handle].vue +58 -0
- package/app/pages/privacy-policy.vue +46 -0
- package/app/pages/resetpassword.vue +138 -0
- package/app/pages/resetverificationcode.vue +163 -0
- package/app/pages/result.vue +203 -0
- package/app/pages/return-policy.vue +50 -0
- package/app/pages/search.vue +100 -0
- package/app/pages/shipping-policy.vue +46 -0
- package/app/pages/user-profile/components/AddAddressModal.vue +452 -0
- package/app/pages/user-profile/components/Whishlist.vue +137 -0
- package/app/pages/user-profile/components/left-bar.vue +233 -0
- package/app/pages/user-profile/components/orders.vue +162 -0
- package/app/pages/user-profile/components/profile-details.vue +566 -0
- package/app/pages/user-profile/components/settings.vue +194 -0
- package/app/pages/user-profile/index.vue +252 -0
- package/app/pages/verificationcode.vue +188 -0
- package/app/plugins/analytics.client.js +99 -0
- package/app/plugins/geo.ts +24 -0
- package/app/plugins/reveal.client.js +53 -0
- package/app/plugins/toast.js +10 -0
- package/app/plugins/vue-query.ts +5 -0
- package/app/services/ApiServices.js +74 -0
- package/app/services/AuthBus.js +162 -0
- package/app/services/Authentication/AuthenticationService.js +133 -0
- package/app/services/Authentication/TokenService.js +62 -0
- package/app/services/CartServices.js +91 -0
- package/app/services/CartToken.js +26 -0
- package/app/services/CategoryServices.js +54 -0
- package/app/services/CheckoutServices.js +113 -0
- package/app/services/HomeServices.js +133 -0
- package/app/services/NewsletterServices.js +29 -0
- package/app/services/PaymentServices.js +71 -0
- package/app/services/PoliciesServices.js +16 -0
- package/app/services/ProductServices.js +79 -0
- package/app/services/SearchServices.js +18 -0
- package/app/services/UserServices.js +105 -0
- package/app/services/UtilsServices.js +46 -0
- package/app/services/WishlistServices.js +51 -0
- package/app/services/storefront.service.js +57 -0
- package/app/stores/userStore.js +13 -0
- package/app/theme/categoryTree.js +68 -0
- package/app/theme/controls.js +88 -0
- package/app/theme/registry.js +56 -0
- package/app/utils/analytics.js +864 -0
- package/app/utils/device.js +50 -0
- package/app/utils/eventBus.js +24 -0
- package/app/utils/icon-presets.js +24 -0
- package/app/utils/productOffers.js +118 -0
- package/app/utils/requestCache.js +36 -0
- package/app/utils/scrollToError.js +59 -0
- package/app/utils/toPlainError.js +30 -0
- package/bin/osimart-theme.mjs +259 -0
- package/nuxt/osi-sections.ts +56 -0
- package/nuxt.config.ts +60 -0
- package/package.json +43 -3
- package/public/css/input.css +29 -0
- package/public/css/output.css +2716 -0
- package/public/favicon-32x32.png +0 -0
- package/public/robots.txt +2 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import ProductServices from "#osi/services/ProductServices";
|
|
2
|
+
import HomeServices from "#osi/services/HomeServices";
|
|
3
|
+
import { flattenCategories } from "#osi/theme/categoryTree";
|
|
4
|
+
import { cachedRequest } from "#osi/utils/requestCache";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Access the rows a section needs.
|
|
8
|
+
*
|
|
9
|
+
* The backend resolves SOURCE into `section.data` before the payload leaves the
|
|
10
|
+
* server. That's deliberate: a six-section homepage is one request instead of
|
|
11
|
+
* seven, and SOURCE=MANUAL would otherwise be one request per block.
|
|
12
|
+
*
|
|
13
|
+
* So this is mostly an accessor. The fetch path exists for two cases:
|
|
14
|
+
*
|
|
15
|
+
* 1. Builder preview — STATE arrives over postMessage with no `data`, because
|
|
16
|
+
* the editor never resolves products or classifications.
|
|
17
|
+
* 2. A CLASSIFICATION_LIST set to MANUAL whose blocks arrived without their
|
|
18
|
+
* category/collection expanded (see resolveManualClassifications). On a
|
|
19
|
+
* backend that resolves MANUAL server-side this never fires; until then it
|
|
20
|
+
* is what makes manual selection render at all.
|
|
21
|
+
*
|
|
22
|
+
* CHANGED in this revision:
|
|
23
|
+
* * isCollectionSource()'s eight-key heuristic is gone. SOURCE is now an
|
|
24
|
+
* explicit MANUAL | ALL_CATEGORIES | ALL_COLLECTIONS, so guessing from
|
|
25
|
+
* TYPE / TARGET / SELECTION / MODE / RESOURCE_TYPE is no longer needed. Two
|
|
26
|
+
* legacy keys are still read for sections created before that landed.
|
|
27
|
+
* * MANUAL is handled: picks are read off the blocks, and any that the
|
|
28
|
+
* backend didn't expand are matched against the catalog lists, which are
|
|
29
|
+
* fetched once per page and shared by every section on it.
|
|
30
|
+
* * The `current.data` early return is now conditional — a live section can
|
|
31
|
+
* carry `data` and still have unresolved manual picks.
|
|
32
|
+
*
|
|
33
|
+
* @param {object|Ref|(() => object)} section pass a ref/computed/getter (e.g.
|
|
34
|
+
* `() => props.section`) — a plain object is captured once and won't see the
|
|
35
|
+
* new section pushed over postMessage on the next builder edit.
|
|
36
|
+
*/
|
|
37
|
+
export function useSectionData(section) {
|
|
38
|
+
const fetched = ref(null);
|
|
39
|
+
const loading = ref(false);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Catalog rows, shared by every section on the page.
|
|
43
|
+
*
|
|
44
|
+
* useState, not a module-level ref: one Nuxt process serves every tenant, and
|
|
45
|
+
* a module ref would leak one store's categories into another store's SSR
|
|
46
|
+
* response. Only ever written on the client, where that can't happen anyway —
|
|
47
|
+
* but the rule holds regardless of where the write happens today.
|
|
48
|
+
*/
|
|
49
|
+
const catalog = useState("osi-classification-catalog", () => ({
|
|
50
|
+
CATEGORY: null,
|
|
51
|
+
COLLECTION: null,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Placeholders enter here and nowhere else.
|
|
56
|
+
*
|
|
57
|
+
* This is the one point every list section reads its rows from, and the rows
|
|
58
|
+
* are still in raw API shape at this stage — so useClassificationItems,
|
|
59
|
+
* ProductCard, PARENTS_ONLY, LIMIT and HIDE_EMPTY all treat a placeholder
|
|
60
|
+
* exactly like a real row and need no branch of their own. Filling further
|
|
61
|
+
* downstream would mean teaching each of them separately.
|
|
62
|
+
*
|
|
63
|
+
* usePlaceholders returns the real rows untouched whenever there are any, and
|
|
64
|
+
* returns nothing at all outside the builder iframe.
|
|
65
|
+
*/
|
|
66
|
+
const placeholders = usePlaceholders();
|
|
67
|
+
|
|
68
|
+
const rawProducts = computed(
|
|
69
|
+
() => fetched.value?.products ?? toValue(section)?.data?.products ?? []
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Categories are flattened whichever way they arrived. The client fetch
|
|
74
|
+
* always flattened; the backend's resolved `data.classifications` didn't, so
|
|
75
|
+
* on a first load (server data) subcategories nested under their parents
|
|
76
|
+
* were invisible, and only appeared once an edit in the dashboard triggered
|
|
77
|
+
* the client fetch — "Top-level only" off seemed to need clicking twice.
|
|
78
|
+
*/
|
|
79
|
+
const rawClassifications = computed(() =>
|
|
80
|
+
flattenCategories(fetched.value?.classifications ?? toValue(section)?.data?.classifications ?? [])
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const products = computed(() => {
|
|
84
|
+
const current = toValue(section);
|
|
85
|
+
if (current?.type !== "PRODUCT_LIST") return rawProducts.value;
|
|
86
|
+
return placeholders.products(
|
|
87
|
+
rawProducts.value,
|
|
88
|
+
Number(current.settings?.LIMIT) || 8
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const classifications = computed(() => {
|
|
93
|
+
const current = toValue(section);
|
|
94
|
+
if (current?.type !== "CLASSIFICATION_LIST") return rawClassifications.value;
|
|
95
|
+
|
|
96
|
+
return placeholders.classifications(
|
|
97
|
+
rawClassifications.value,
|
|
98
|
+
Number(current.settings?.LIMIT) || 6
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Manually picked product ids.
|
|
104
|
+
*
|
|
105
|
+
* Reads the relation and the setting. Which one a block carries depends on
|
|
106
|
+
* how the editor persisted the pick, and a preview that only understands one
|
|
107
|
+
* of them renders an empty grid while the panel shows products selected.
|
|
108
|
+
*/
|
|
109
|
+
function manualProductIds(current) {
|
|
110
|
+
const idOf = (value) => (value && typeof value === "object" ? value.id : value);
|
|
111
|
+
// The panel's multi-select first (pick order), then older sections'
|
|
112
|
+
// PRODUCT blocks. Header and card blocks carry no product.
|
|
113
|
+
const ids = (current?.settings?.PRODUCTS ?? []).map(idOf).filter(Boolean);
|
|
114
|
+
for (const b of current?.blocks ?? []) {
|
|
115
|
+
if (b.type !== "PRODUCT") continue;
|
|
116
|
+
const id = idOf(b.relations?.product ?? b.settings?.PRODUCT ?? null);
|
|
117
|
+
if (id && !ids.some((x) => String(x) === String(id))) ids.push(id);
|
|
118
|
+
}
|
|
119
|
+
return ids;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A MANUAL product list whose resolved rows don't hold every pick — the
|
|
124
|
+
* backend resolved picks from PRODUCT blocks only, or the payload predates
|
|
125
|
+
* the pick. Fetched on the client, like manual classifications.
|
|
126
|
+
*/
|
|
127
|
+
function needsManualProducts(current) {
|
|
128
|
+
if (current?.type !== "PRODUCT_LIST") return false;
|
|
129
|
+
if (String(current.settings?.SOURCE ?? "").toUpperCase() !== "MANUAL") return false;
|
|
130
|
+
const ids = manualProductIds(current);
|
|
131
|
+
if (!ids.length) return false;
|
|
132
|
+
const have = new Set((current.data?.products ?? []).map((p) => String(p.id)));
|
|
133
|
+
return ids.some((id) => !have.has(String(id)));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------- classification
|
|
137
|
+
|
|
138
|
+
function kindOf(value, fallback = "CATEGORY") {
|
|
139
|
+
return String(value ?? fallback)
|
|
140
|
+
.toUpperCase()
|
|
141
|
+
.includes("COLLECTION")
|
|
142
|
+
? "COLLECTION"
|
|
143
|
+
: "CATEGORY";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function classificationSource(current) {
|
|
147
|
+
const s = current?.settings ?? {};
|
|
148
|
+
// CLASSIFICATION_TYPE and TYPE: sections created before SOURCE gained its
|
|
149
|
+
// MANUAL member. Everything else the old heuristic sniffed was speculative.
|
|
150
|
+
const raw = String(s.SOURCE ?? s.CLASSIFICATION_TYPE ?? s.TYPE ?? "").toUpperCase();
|
|
151
|
+
|
|
152
|
+
// FEATURED has no SOURCE field — it always resolves one explicit pick.
|
|
153
|
+
if (String(s.LAYOUT ?? "").toUpperCase() === "FEATURED") return "MANUAL";
|
|
154
|
+
// Picked in the panel: resolve exactly those (either kind), not a list.
|
|
155
|
+
if ((s.CLASSIFICATIONS ?? []).length) return "MANUAL";
|
|
156
|
+
|
|
157
|
+
if (raw === "MANUAL") return "MANUAL";
|
|
158
|
+
if (raw.includes("COLLECTION")) return "ALL_COLLECTIONS";
|
|
159
|
+
if (raw.includes("CATEGOR")) return "ALL_CATEGORIES";
|
|
160
|
+
|
|
161
|
+
// No source at all + blocks that point at a category / collection is the
|
|
162
|
+
// old manual behaviour. The header and card are blocks too — they don't.
|
|
163
|
+
const pointsAtOne = (b) =>
|
|
164
|
+
!!(b?.settings?.CLASSIFICATION || b?.relations?.category || b?.relations?.collection);
|
|
165
|
+
if ((current?.blocks ?? []).some(pointsAtOne)) return "MANUAL";
|
|
166
|
+
return "ALL_CATEGORIES";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Everything a merchant picked by hand, in render order.
|
|
171
|
+
*
|
|
172
|
+
* Three places, because there are three ways to pick:
|
|
173
|
+
* FEATURED_CLASSIFICATION the single pick for the featured layout
|
|
174
|
+
* CLASSIFICATIONS the panel's multi-select (the normal path)
|
|
175
|
+
* blocks legacy, and where per-item overrides live
|
|
176
|
+
*
|
|
177
|
+
* Blocks go last and win nothing on their own — they're merged, not replaced,
|
|
178
|
+
* so a section created before the CLASSIFICATIONS field existed still renders.
|
|
179
|
+
*/
|
|
180
|
+
function manualPicks(current) {
|
|
181
|
+
const settings = current?.settings ?? {};
|
|
182
|
+
const picks = [];
|
|
183
|
+
|
|
184
|
+
const single = settings.FEATURED_CLASSIFICATION;
|
|
185
|
+
if (single && typeof single === "object" && single.id != null) {
|
|
186
|
+
picks.push({ kind: kindOf(single.kind ?? single.type), id: single.id, resolved: !!single.name });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const entry of settings.CLASSIFICATIONS ?? []) {
|
|
190
|
+
if (!entry) continue;
|
|
191
|
+
if (typeof entry === "object") {
|
|
192
|
+
if (entry.id == null) continue;
|
|
193
|
+
picks.push({ kind: kindOf(entry.kind ?? entry.type), id: entry.id, resolved: !!entry.name });
|
|
194
|
+
} else {
|
|
195
|
+
// "CATEGORY:12" — the encoded form, in case a value was persisted
|
|
196
|
+
// straight off the select before decode() ran.
|
|
197
|
+
const [kind, id] = String(entry).split(":");
|
|
198
|
+
if (id) picks.push({ kind: kindOf(kind), id: /^\d+$/.test(id) ? Number(id) : id, resolved: false });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return [...picks, ...blockPicks(current)];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The { kind, id } a merchant picked on a block, in block order. */
|
|
206
|
+
function blockPicks(current) {
|
|
207
|
+
return (current?.blocks ?? [])
|
|
208
|
+
.map((block) => {
|
|
209
|
+
const relations = block.relations ?? {};
|
|
210
|
+
if (relations.category) {
|
|
211
|
+
return { kind: "CATEGORY", id: relations.category.id, resolved: true };
|
|
212
|
+
}
|
|
213
|
+
if (relations.collection) {
|
|
214
|
+
return { kind: "COLLECTION", id: relations.collection.id, resolved: true };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const raw = block.settings?.CLASSIFICATION ?? relations.classification ?? null;
|
|
218
|
+
if (!raw) return null;
|
|
219
|
+
|
|
220
|
+
if (typeof raw === "object") {
|
|
221
|
+
// The picker may have stored a snapshot rather than a bare reference.
|
|
222
|
+
// A row that already carries a name needs no lookup.
|
|
223
|
+
return { kind: kindOf(raw.kind ?? raw.type), id: raw.id, resolved: !!raw.name };
|
|
224
|
+
}
|
|
225
|
+
return { kind: "CATEGORY", id: raw, resolved: false };
|
|
226
|
+
})
|
|
227
|
+
.filter((pick) => pick && pick.id != null);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Categories come back as a tree; a merchant can pick a child.
|
|
232
|
+
* Flattens defensively — the key name for children differs between endpoints.
|
|
233
|
+
*/
|
|
234
|
+
function flatten(rows) {
|
|
235
|
+
return flattenCategories(rows);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function loadCatalog(kind) {
|
|
239
|
+
if (catalog.value[kind]) return catalog.value[kind];
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const response =
|
|
243
|
+
kind === "COLLECTION"
|
|
244
|
+
? await HomeServices.getCollections()
|
|
245
|
+
: await HomeServices.getCategories();
|
|
246
|
+
|
|
247
|
+
const rows = flatten(response?.results ?? response ?? []).map((row) => ({
|
|
248
|
+
...row,
|
|
249
|
+
// Stamp the kind: nothing downstream can tell a category row from a
|
|
250
|
+
// collection row by shape, and the URL prefix depends on it.
|
|
251
|
+
kind: row.kind ?? kind,
|
|
252
|
+
}));
|
|
253
|
+
|
|
254
|
+
catalog.value = { ...catalog.value, [kind]: rows };
|
|
255
|
+
return rows;
|
|
256
|
+
} catch (e) {
|
|
257
|
+
console.warn(`[useSectionData] loadCatalog(${kind}) failed (e.g. 404), using empty fallback:`, e);
|
|
258
|
+
catalog.value = { ...catalog.value, [kind]: [] };
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Fill in manual picks the backend didn't expand.
|
|
265
|
+
*
|
|
266
|
+
* One request per kind, cached for the page — a homepage with three manual
|
|
267
|
+
* category rows costs one call, not one per row and certainly not one per
|
|
268
|
+
* picked category.
|
|
269
|
+
*/
|
|
270
|
+
async function resolveManualClassifications(current) {
|
|
271
|
+
const picks = manualPicks(current);
|
|
272
|
+
if (!picks.length) return [];
|
|
273
|
+
|
|
274
|
+
const kinds = [...new Set(picks.filter((p) => !p.resolved).map((p) => p.kind))];
|
|
275
|
+
if (!kinds.length) return [];
|
|
276
|
+
|
|
277
|
+
const loaded = await Promise.all(kinds.map((kind) => loadCatalog(kind)));
|
|
278
|
+
const index = new Map();
|
|
279
|
+
loaded.flat().forEach((row) => index.set(`${row.kind}:${row.id}`, row));
|
|
280
|
+
|
|
281
|
+
// Block order is the merchant's order. Keep it.
|
|
282
|
+
return picks.map((pick) => index.get(`${pick.kind}:${pick.id}`)).filter(Boolean);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* True when a section that already has `data` still needs a client-side
|
|
287
|
+
* lookup — a MANUAL list whose blocks arrived as bare references.
|
|
288
|
+
*/
|
|
289
|
+
function needsManualResolve(current) {
|
|
290
|
+
if (current?.type !== "CLASSIFICATION_LIST") return false;
|
|
291
|
+
if (classificationSource(current) !== "MANUAL") return false;
|
|
292
|
+
|
|
293
|
+
const picks = manualPicks(current);
|
|
294
|
+
if (!picks.length) return false;
|
|
295
|
+
if (picks.every((pick) => pick.resolved)) return false;
|
|
296
|
+
|
|
297
|
+
// Already matched by an earlier run.
|
|
298
|
+
const known = new Set(
|
|
299
|
+
(current.data?.classifications ?? []).map((row) => `${kindOf(row.kind ?? row.type)}:${row.id}`)
|
|
300
|
+
);
|
|
301
|
+
return picks.some((pick) => !known.has(`${pick.kind}:${pick.id}`));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ---------------------------------------------------------------- resolve
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Everything that changes what should be on screen, as one string.
|
|
308
|
+
*
|
|
309
|
+
* Watching the section object itself would refetch on every keystroke in the
|
|
310
|
+
* heading field; watching SOURCE alone would miss a category being added to a
|
|
311
|
+
* MANUAL list.
|
|
312
|
+
*/
|
|
313
|
+
const signature = computed(() => {
|
|
314
|
+
const current = toValue(section);
|
|
315
|
+
if (!current) return "";
|
|
316
|
+
|
|
317
|
+
if (current.type === "PRODUCT_LIST") {
|
|
318
|
+
if (current.data && !needsManualProducts(current)) return "";
|
|
319
|
+
const s = current.settings ?? {};
|
|
320
|
+
return JSON.stringify([
|
|
321
|
+
"PRODUCT_LIST",
|
|
322
|
+
s.SOURCE ?? "LATEST",
|
|
323
|
+
s.LIMIT ?? 8,
|
|
324
|
+
manualProductIds(current),
|
|
325
|
+
]);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (current.type === "CLASSIFICATION_LIST") {
|
|
329
|
+
if (current.data && !needsManualResolve(current)) return "";
|
|
330
|
+
const s = current.settings ?? {};
|
|
331
|
+
return JSON.stringify([
|
|
332
|
+
"CLASSIFICATION_LIST",
|
|
333
|
+
classificationSource(current),
|
|
334
|
+
s.LIMIT ?? null,
|
|
335
|
+
manualPicks(current).map((pick) => `${pick.kind}:${pick.id}`),
|
|
336
|
+
]);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return "";
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Guards against an out-of-order response overwriting a newer one: the
|
|
343
|
+
// merchant drags the LIMIT slider, six requests go out, and without this the
|
|
344
|
+
// slowest one wins.
|
|
345
|
+
let requestToken = 0;
|
|
346
|
+
let debounceTimer = null;
|
|
347
|
+
|
|
348
|
+
async function resolve() {
|
|
349
|
+
const current = toValue(section);
|
|
350
|
+
if (!current) return;
|
|
351
|
+
if (current.type !== "PRODUCT_LIST" && current.type !== "CLASSIFICATION_LIST") return;
|
|
352
|
+
if (current.data && !needsManualResolve(current) && !needsManualProducts(current)) return;
|
|
353
|
+
|
|
354
|
+
const settings = current.settings ?? {};
|
|
355
|
+
const token = ++requestToken;
|
|
356
|
+
|
|
357
|
+
loading.value = true;
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
if (current.type === "PRODUCT_LIST") {
|
|
361
|
+
const limit = Number(settings.LIMIT) || 8;
|
|
362
|
+
let response;
|
|
363
|
+
|
|
364
|
+
if (settings.SOURCE === "MANUAL") {
|
|
365
|
+
const ids = manualProductIds(current);
|
|
366
|
+
// Not a no-op: clearing the last block has to empty the grid, not
|
|
367
|
+
// leave the previous fetch's products sitting there.
|
|
368
|
+
if (!ids.length) {
|
|
369
|
+
if (token === requestToken) fetched.value = { products: [] };
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
// Cached per tab (utils/requestCache): coming back to a page reuses it.
|
|
373
|
+
response = await cachedRequest(`products:ids:${ids.join(",")}`, () => ProductServices.getProductsByIds(ids));
|
|
374
|
+
} else {
|
|
375
|
+
const params = { page_size: limit };
|
|
376
|
+
// Best-effort mapping of the other sources. An unknown value falls
|
|
377
|
+
// through to the default ordering rather than erroring — the preview's
|
|
378
|
+
// job is to show layout, and being approximately right beats blank.
|
|
379
|
+
if (settings.SOURCE === "ON_SALE") params.on_sale = true;
|
|
380
|
+
if (settings.SOURCE === "BEST_SELLERS") params.ordering = "-sales_count";
|
|
381
|
+
if (settings.SOURCE === "LATEST") params.ordering = "-created_at";
|
|
382
|
+
response = await cachedRequest(`products:list:${JSON.stringify(params)}`, () => ProductServices.listProducts(params));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (token !== requestToken) return;
|
|
386
|
+
|
|
387
|
+
const rows = response?.results ?? response ?? [];
|
|
388
|
+
fetched.value = {
|
|
389
|
+
products: settings.SOURCE === "MANUAL" ? rows : rows.slice(0, limit),
|
|
390
|
+
};
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ---- CLASSIFICATION_LIST ----
|
|
395
|
+
const source = classificationSource(current);
|
|
396
|
+
|
|
397
|
+
if (source === "MANUAL") {
|
|
398
|
+
const rows = await resolveManualClassifications(current);
|
|
399
|
+
if (token !== requestToken) return;
|
|
400
|
+
|
|
401
|
+
// Merge with whatever the backend did expand, so a half-resolved
|
|
402
|
+
// section doesn't lose the half that worked.
|
|
403
|
+
const existing = current.data?.classifications ?? [];
|
|
404
|
+
const seen = new Set();
|
|
405
|
+
fetched.value = {
|
|
406
|
+
classifications: [...existing, ...rows].filter((row) => {
|
|
407
|
+
const key = `${kindOf(row.kind ?? row.type)}:${row.id}`;
|
|
408
|
+
if (seen.has(key)) return false;
|
|
409
|
+
seen.add(key);
|
|
410
|
+
return true;
|
|
411
|
+
}),
|
|
412
|
+
};
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const kind = source === "ALL_COLLECTIONS" ? "COLLECTION" : "CATEGORY";
|
|
417
|
+
const rows = await loadCatalog(kind);
|
|
418
|
+
if (token !== requestToken) return;
|
|
419
|
+
|
|
420
|
+
// LIMIT and PARENTS_ONLY are applied in useClassificationItems, so the
|
|
421
|
+
// preview and the live page filter identically. Don't slice here.
|
|
422
|
+
fetched.value = { classifications: rows };
|
|
423
|
+
} catch (e) {
|
|
424
|
+
// A preview that can't reach the catalog should still show layout with dummy data.
|
|
425
|
+
if (token === requestToken) {
|
|
426
|
+
console.warn("[useSectionData] resolve failed:", e);
|
|
427
|
+
if (current.type === "PRODUCT_LIST") {
|
|
428
|
+
fetched.value = { products: [] };
|
|
429
|
+
} else if (current.type === "CLASSIFICATION_LIST") {
|
|
430
|
+
fetched.value = { classifications: [] };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
} finally {
|
|
434
|
+
if (token === requestToken) loading.value = false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (import.meta.client) {
|
|
439
|
+
watch(
|
|
440
|
+
signature,
|
|
441
|
+
(value) => {
|
|
442
|
+
if (!value) return;
|
|
443
|
+
clearTimeout(debounceTimer);
|
|
444
|
+
// The editor pushes STATE on every slider frame; without this a drag
|
|
445
|
+
// across the LIMIT range fires a request per pixel.
|
|
446
|
+
debounceTimer = setTimeout(resolve, 200);
|
|
447
|
+
},
|
|
448
|
+
{ immediate: true }
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
onScopeDispose(() => clearTimeout(debounceTimer));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
products,
|
|
456
|
+
classifications,
|
|
457
|
+
loading,
|
|
458
|
+
/** Kept for the existing onMounted calls in the section components. Now a
|
|
459
|
+
* no-op in practice — the watcher above has already run. */
|
|
460
|
+
resolveForPreview: resolve,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SectionWidth -> max-width.
|
|
3
|
+
*
|
|
4
|
+
* Exported because the footer takes the same enum (FOOTER_CONTAINER) but isn't
|
|
5
|
+
* a section, so it can't go through useSectionShell. It previously carried its
|
|
6
|
+
* own copy with different values, which made a NARROW footer a different width
|
|
7
|
+
* from a NARROW section directly above it.
|
|
8
|
+
*/
|
|
9
|
+
export const SECTION_WIDTHS = {
|
|
10
|
+
NARROW: "720px",
|
|
11
|
+
NORMAL: "var(--osi-page-width)",
|
|
12
|
+
WIDE: "1600px",
|
|
13
|
+
FULL: "100%",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const WIDTHS = SECTION_WIDTHS;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The settings every section shares.
|
|
20
|
+
*
|
|
21
|
+
* COMMON_STYLE_SETTINGS is spliced into nearly every schema on the backend —
|
|
22
|
+
* COLOR_SCHEME, CONTAINER, HEADING_ALIGNMENT, PADDING_*, BORDER_RADIUS,
|
|
23
|
+
* CUSTOM_CLASS. Handling them once here means a new section type only has to
|
|
24
|
+
* implement what makes it different.
|
|
25
|
+
*
|
|
26
|
+
* @param {object|Ref|(() => object)} section the section passed to every
|
|
27
|
+
* section component. Pass a ref/computed/getter (e.g. `() => props.section`),
|
|
28
|
+
* not the raw object — a plain object is captured once at call time and
|
|
29
|
+
* never sees the new section pushed over postMessage on the next builder edit.
|
|
30
|
+
*/
|
|
31
|
+
export function useSectionShell(section) {
|
|
32
|
+
const t = useT();
|
|
33
|
+
const s = computed(() => toValue(section)?.settings ?? {});
|
|
34
|
+
const resolved = useColorScheme(computed(() => s.value.COLOR_SCHEME));
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Padding, one value per side, with optional phone values.
|
|
38
|
+
*
|
|
39
|
+
* PADDING_TOP / PADDING_BOTTOM desktop
|
|
40
|
+
* PADDING_LEFT / PADDING_RIGHT desktop; empty = PADDING_X
|
|
41
|
+
* PADDING_X both sides (older sections)
|
|
42
|
+
* PADDING_*_MOBILE phones; empty = the desktop value
|
|
43
|
+
*
|
|
44
|
+
* Written as variables plus `padding: var(--osi-sec-p*-now, <desktop>)`. The
|
|
45
|
+
* -now variables are switched by a media query in app.vue, which is the only
|
|
46
|
+
* way an inline style can have a phone value. A section root without the
|
|
47
|
+
* osi-section class still gets its desktop padding from the fallback.
|
|
48
|
+
*
|
|
49
|
+
* LEFT/RIGHT are physical on purpose: the merchant sees "left" and "right"
|
|
50
|
+
* in the panel and expects exactly that side, in RTL too.
|
|
51
|
+
*/
|
|
52
|
+
const px = (value) => {
|
|
53
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
54
|
+
const n = Number(value);
|
|
55
|
+
return Number.isFinite(n) ? `${n}px` : undefined;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const padding = computed(() => {
|
|
59
|
+
const v = s.value;
|
|
60
|
+
const x = px(v.PADDING_X) ?? "16px";
|
|
61
|
+
const top = px(v.PADDING_TOP) ?? "48px";
|
|
62
|
+
const bottom = px(v.PADDING_BOTTOM) ?? "48px";
|
|
63
|
+
const left = px(v.PADDING_LEFT) ?? x;
|
|
64
|
+
const right = px(v.PADDING_RIGHT) ?? x;
|
|
65
|
+
return {
|
|
66
|
+
"--osi-sec-pt": top,
|
|
67
|
+
"--osi-sec-pb": bottom,
|
|
68
|
+
"--osi-sec-pl": left,
|
|
69
|
+
"--osi-sec-pr": right,
|
|
70
|
+
"--osi-sec-pt-m": px(v.PADDING_TOP_MOBILE),
|
|
71
|
+
"--osi-sec-pb-m": px(v.PADDING_BOTTOM_MOBILE),
|
|
72
|
+
"--osi-sec-pl-m": px(v.PADDING_LEFT_MOBILE),
|
|
73
|
+
"--osi-sec-pr-m": px(v.PADDING_RIGHT_MOBILE),
|
|
74
|
+
paddingTop: `var(--osi-sec-pt-now, ${top})`,
|
|
75
|
+
paddingBottom: `var(--osi-sec-pb-now, ${bottom})`,
|
|
76
|
+
paddingLeft: `var(--osi-sec-pl-now, ${left})`,
|
|
77
|
+
paddingRight: `var(--osi-sec-pr-now, ${right})`,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const shellStyle = computed(() => {
|
|
82
|
+
const style = {
|
|
83
|
+
...resolved.value.style,
|
|
84
|
+
// BACKGROUND_COLOR overrides the scheme's background for this section
|
|
85
|
+
// only; text and button colours still come from the scheme.
|
|
86
|
+
...(s.value.BACKGROUND_COLOR ? { "--osi-bg": s.value.BACKGROUND_COLOR } : {}),
|
|
87
|
+
background: "var(--osi-bg)",
|
|
88
|
+
color: "var(--osi-text)",
|
|
89
|
+
...padding.value,
|
|
90
|
+
borderRadius: `${s.value.BORDER_RADIUS ?? 0}px`,
|
|
91
|
+
};
|
|
92
|
+
for (const key of Object.keys(style)) if (style[key] === undefined) delete style[key];
|
|
93
|
+
return style;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const innerStyle = computed(() => ({
|
|
97
|
+
maxWidth: WIDTHS[s.value.CONTAINER ?? "NORMAL"] ?? WIDTHS.NORMAL,
|
|
98
|
+
marginInline: "auto",
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
// 'LEFT' | 'CENTER' | 'RIGHT' -> a CSS text-align value. Uses start/end so it
|
|
102
|
+
// flips correctly in Arabic instead of pinning to the physical left.
|
|
103
|
+
const align = computed(() => {
|
|
104
|
+
const value = s.value.HEADING_ALIGNMENT ?? "LEFT";
|
|
105
|
+
if (value === "CENTER") return "center";
|
|
106
|
+
return value === "RIGHT" ? "end" : "start";
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const customClass = computed(() => s.value.CUSTOM_CLASS ?? "");
|
|
110
|
+
const heading = computed(() => t(s.value.HEADING));
|
|
111
|
+
|
|
112
|
+
return { s, t, shellStyle, innerStyle, align, customClass, heading, scheme: resolved };
|
|
113
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cart drawer and mobile menu visibility.
|
|
3
|
+
*
|
|
4
|
+
* Two changes from the version this replaces:
|
|
5
|
+
*
|
|
6
|
+
* - `process.client` -> `import.meta.client`. `process` isn't defined in the
|
|
7
|
+
* browser bundle under Nuxt 3's default, so the old guard threw on the
|
|
8
|
+
* client and the body was never locked at all.
|
|
9
|
+
* - Scroll locking goes through useBodyScrollLock() instead of writing
|
|
10
|
+
* `document.body.style.overflow` directly. Opening the cart over an open
|
|
11
|
+
* mobile menu and closing one used to unlock the page under the other.
|
|
12
|
+
*
|
|
13
|
+
* State is useState, so the drawer survives a page component swapping out
|
|
14
|
+
* during navigation — closing it is a deliberate act (see the route watcher in
|
|
15
|
+
* the layout), not a side effect of the page unmounting.
|
|
16
|
+
*/
|
|
17
|
+
export function useSidebar() {
|
|
18
|
+
const isCartSidebarVisible = useState("cart-sidebar-visible", () => false);
|
|
19
|
+
const isMobileSidebarVisible = useState("mobile-sidebar-visible", () => false);
|
|
20
|
+
|
|
21
|
+
const anyOpen = computed(
|
|
22
|
+
() => isCartSidebarVisible.value || isMobileSidebarVisible.value
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// SIDEBAR_LOCK_SCROLL is honoured for the mobile menu only. The cart drawer
|
|
26
|
+
// always locks: it has its own scrolling line-item list, and a page moving
|
|
27
|
+
// underneath it makes that list impossible to use on touch. Reading the
|
|
28
|
+
// setting through useThemeGroup means an older theme without the key falls
|
|
29
|
+
// back to true, which is the behaviour this replaces.
|
|
30
|
+
const sidebarSettings = useThemeGroup("SIDEBAR");
|
|
31
|
+
const shouldLock = computed(
|
|
32
|
+
() =>
|
|
33
|
+
isCartSidebarVisible.value ||
|
|
34
|
+
(isMobileSidebarVisible.value && sidebarSettings.value.SIDEBAR_LOCK_SCROLL !== false)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// One lock for both drawers: the page needs to stay locked while either is
|
|
38
|
+
// open, and unlock exactly once when the last one closes.
|
|
39
|
+
useBodyScrollLock(shouldLock);
|
|
40
|
+
|
|
41
|
+
function toggleCartSidebar(visible) {
|
|
42
|
+
isCartSidebarVisible.value =
|
|
43
|
+
typeof visible === "boolean" ? visible : !isCartSidebarVisible.value;
|
|
44
|
+
|
|
45
|
+
// Never both at once — they'd stack and the backdrop would trap focus.
|
|
46
|
+
if (isCartSidebarVisible.value) isMobileSidebarVisible.value = false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toggleMobileSidebar(visible) {
|
|
50
|
+
isMobileSidebarVisible.value =
|
|
51
|
+
typeof visible === "boolean" ? visible : !isMobileSidebarVisible.value;
|
|
52
|
+
|
|
53
|
+
if (isMobileSidebarVisible.value) isCartSidebarVisible.value = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function closeAll() {
|
|
57
|
+
isCartSidebarVisible.value = false;
|
|
58
|
+
isMobileSidebarVisible.value = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
isCartSidebarVisible,
|
|
63
|
+
isMobileSidebarVisible,
|
|
64
|
+
anyOpen,
|
|
65
|
+
toggleCartSidebar,
|
|
66
|
+
toggleMobileSidebar,
|
|
67
|
+
closeAll,
|
|
68
|
+
};
|
|
69
|
+
}
|