@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,473 @@
|
|
|
1
|
+
import { useClassificationPage } from "#osi/composables/useClassificationPage";
|
|
2
|
+
import { ClassificationPart } from "#components";
|
|
3
|
+
|
|
4
|
+
export default function useLogic({ props }) {
|
|
5
|
+
/**
|
|
6
|
+
* The settings the listing runs on. With blocks, the Toolbar, Categories and
|
|
7
|
+
* Product grid blocks hold their parts' settings (same IDs as the section
|
|
8
|
+
* had) — laid over the section's, so everything below reads them as before.
|
|
9
|
+
* Without blocks (older sections) the section's own.
|
|
10
|
+
*/
|
|
11
|
+
const LISTING_BLOCKS = ["CLASSIFICATION_TOOLBAR", "CLASSIFICATION_NAV", "PRODUCT_GRID"];
|
|
12
|
+
const effectiveSection = computed(() => {
|
|
13
|
+
const blocks = (props.section.blocks ?? []).filter((b) => !b.disabled && LISTING_BLOCKS.includes(b.type));
|
|
14
|
+
if (!blocks.length) return props.section;
|
|
15
|
+
const settings = { ...(props.section.settings ?? {}) };
|
|
16
|
+
for (const b of blocks) Object.assign(settings, b.settings ?? {});
|
|
17
|
+
return { ...props.section, settings };
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const { s, t, shellStyle, innerStyle, align, customClass, scheme } = useSectionShell(() => effectiveSection.value);
|
|
21
|
+
const page = useClassificationPage(() => effectiveSection.value);
|
|
22
|
+
const {
|
|
23
|
+
kind, terms, title, description, image, breadcrumbs, navItems, sortOptions, ordering,
|
|
24
|
+
selectedValues, filters, filtersPending, activeFilters, products, total,
|
|
25
|
+
pageCount, pagination, pending, hasMore, loadingMore,
|
|
26
|
+
} = page;
|
|
27
|
+
|
|
28
|
+
/* --------------------------------------------------------------- blocks
|
|
29
|
+
* Like Shopify's main collection section: with blocks, they decide what's on
|
|
30
|
+
* the page and where — Breadcrumbs, Title, Description, Image, Categories
|
|
31
|
+
* (anywhere, in Groups), the Toolbar and the Product grid (holding the Card
|
|
32
|
+
* template). The section's settings still run the listing (filters, sort,
|
|
33
|
+
* toolbar look, columns, pagination). No blocks — a store that hasn't updated
|
|
34
|
+
* — the old fixed layout below. */
|
|
35
|
+
const blockMode = computed(() => (props.section.blocks ?? []).length > 0);
|
|
36
|
+
/** The search page's results (toolbar defaults differ). */
|
|
37
|
+
const isResults = computed(() => props.section.type === "MAIN_SEARCH");
|
|
38
|
+
const flowMode = blockMode;
|
|
39
|
+
const PART = ClassificationPart;
|
|
40
|
+
const { tree } = useContentTree(() => props.section, {
|
|
41
|
+
custom: {
|
|
42
|
+
BREADCRUMBS: PART, CLASSIFICATION_TITLE: PART, CLASSIFICATION_DESCRIPTION: PART,
|
|
43
|
+
CLASSIFICATION_IMAGE: PART, CLASSIFICATION_NAV: PART,
|
|
44
|
+
},
|
|
45
|
+
customText: {
|
|
46
|
+
BREADCRUMBS: "CUSTOM", CLASSIFICATION_TITLE: "H2", CLASSIFICATION_DESCRIPTION: "PARAGRAPH",
|
|
47
|
+
},
|
|
48
|
+
raw: ["CLASSIFICATION_TOOLBAR", "PRODUCT_GRID"],
|
|
49
|
+
index: () => props.index,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The page top to bottom: runs of header blocks (in the page container), the
|
|
54
|
+
* toolbar band (full width) and the grid — each with its place (CSS order).
|
|
55
|
+
*/
|
|
56
|
+
const flow = computed(() => {
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const node of tree.value.children) {
|
|
59
|
+
if (node.type === "CLASSIFICATION_TOOLBAR" || node.type === "PRODUCT_GRID") {
|
|
60
|
+
out.push({ kind: node.type, key: node.key, node });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const last = out[out.length - 1];
|
|
64
|
+
if (last?.kind === "BLOCKS") last.nodes.push(node);
|
|
65
|
+
else out.push({ kind: "BLOCKS", key: node.key, nodes: [node] });
|
|
66
|
+
}
|
|
67
|
+
return out.map((part, order) => ({ ...part, order }));
|
|
68
|
+
});
|
|
69
|
+
const orderOf = (kind) => flow.value.find((p) => p.kind === kind)?.order ?? -1;
|
|
70
|
+
/**
|
|
71
|
+
* Search page before anything is searched: nothing — no toolbar, no "no
|
|
72
|
+
* results" (the section is hidden, paddings included). The editor still
|
|
73
|
+
* shows the grid.
|
|
74
|
+
*/
|
|
75
|
+
const { isPreview } = useBuilderPreview();
|
|
76
|
+
const awaitingTerms = computed(() => kind.value === "SEARCH" && !terms.value && !isPreview.value);
|
|
77
|
+
const toolbarOrder = computed(() =>
|
|
78
|
+
awaitingTerms.value ? -1 : flowMode.value ? orderOf("CLASSIFICATION_TOOLBAR") : 1
|
|
79
|
+
);
|
|
80
|
+
const gridOrder = computed(() => (awaitingTerms.value ? -1 : flowMode.value ? orderOf("PRODUCT_GRID") : 2));
|
|
81
|
+
const blockChunks = computed(() => flow.value.filter((p) => p.kind === "BLOCKS"));
|
|
82
|
+
|
|
83
|
+
/** The Product grid's Card template; none: the standard product card. */
|
|
84
|
+
const cardTemplate = computed(() =>
|
|
85
|
+
flow.value.find((p) => p.kind === "PRODUCT_GRID")?.node.children?.find((n) => n.type === "CARD") ?? null
|
|
86
|
+
);
|
|
87
|
+
const { items: cardItems } = useProductItems(() => props.section, products, { all: true });
|
|
88
|
+
const itemsById = computed(() => Object.fromEntries(cardItems.value.map((i) => [i.id, i])));
|
|
89
|
+
|
|
90
|
+
/* ------------------------------------------------------------- settings */
|
|
91
|
+
|
|
92
|
+
const num = (v, d) => {
|
|
93
|
+
const n = Number(v);
|
|
94
|
+
return v === "" || v === null || v === undefined || !Number.isFinite(n) ? d : n;
|
|
95
|
+
};
|
|
96
|
+
const bool = (v, d) => (v === undefined || v === null || v === "" ? d : v === true || v === "true");
|
|
97
|
+
const pick = (v, allowed, d) => (allowed.includes(String(v ?? "").toUpperCase()) ? String(v).toUpperCase() : d);
|
|
98
|
+
|
|
99
|
+
const WEIGHTS = { LIGHT: 300, REGULAR: 400, MEDIUM: 500, SEMIBOLD: 600, BOLD: 700 };
|
|
100
|
+
const CASES = { NONE: "none", UPPERCASE: "uppercase", LOWERCASE: "lowercase", CAPITALIZE: "capitalize" };
|
|
101
|
+
const weight = (v, d) => WEIGHTS[v] ?? (Number.isFinite(Number(v)) && v !== "" && v != null ? Number(v) : d);
|
|
102
|
+
const JUSTIFY = { LEFT: "flex-start", CENTER: "center", RIGHT: "flex-end" };
|
|
103
|
+
|
|
104
|
+
// header
|
|
105
|
+
const showBreadcrumbs = computed(() => bool(s.value.SHOW_BREADCRUMBS, false));
|
|
106
|
+
const showTitle = computed(() => bool(s.value.SHOW_TITLE, true));
|
|
107
|
+
const showDescription = computed(() => bool(s.value.SHOW_DESCRIPTION, true) && !!description.value);
|
|
108
|
+
const bannerStyle = computed(() => {
|
|
109
|
+
const style = pick(s.value.BANNER_STYLE, ["NONE", "ABOVE", "BEHIND"], "NONE");
|
|
110
|
+
return style !== "NONE" && image.value ? style : "NONE";
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// layout
|
|
114
|
+
/**
|
|
115
|
+
* Asking for the navigation or the filters "in the sidebar" IS asking for a
|
|
116
|
+
* sidebar. With the page layout still on Full width that choice used to be
|
|
117
|
+
* dropped silently, which read as the setting not working; a left sidebar is
|
|
118
|
+
* switched on for it instead. An explicit Left / Right choice still wins.
|
|
119
|
+
*/
|
|
120
|
+
const wantsSidebar = computed(
|
|
121
|
+
() =>
|
|
122
|
+
String(s.value.NAV_POSITION ?? "").toUpperCase() === "SIDEBAR" ||
|
|
123
|
+
String(s.value.FILTER_POSITION ?? "").toUpperCase() === "SIDEBAR"
|
|
124
|
+
);
|
|
125
|
+
const pageLayout = computed(() => {
|
|
126
|
+
const chosen = pick(s.value.PAGE_LAYOUT, ["FULL", "SIDEBAR_LEFT", "SIDEBAR_RIGHT"], "FULL");
|
|
127
|
+
return chosen === "FULL" && wantsSidebar.value ? "SIDEBAR_LEFT" : chosen;
|
|
128
|
+
});
|
|
129
|
+
const hasSidebarLayout = computed(() => pageLayout.value !== "FULL");
|
|
130
|
+
|
|
131
|
+
// navigation
|
|
132
|
+
const navPosition = computed(() => {
|
|
133
|
+
const p = pick(s.value.NAV_POSITION, ["ABOVE_TITLE", "BELOW_TITLE", "SIDEBAR"], "BELOW_TITLE");
|
|
134
|
+
return p === "SIDEBAR" && !hasSidebarLayout.value ? "BELOW_TITLE" : p;
|
|
135
|
+
});
|
|
136
|
+
const navAlign = computed(() =>
|
|
137
|
+
pick(s.value.NAV_ALIGNMENT, ["LEFT", "CENTER", "RIGHT"], align.value === "center" ? "CENTER" : "LEFT")
|
|
138
|
+
);
|
|
139
|
+
const navLayout = computed(() => pick(s.value.NAV_LAYOUT, ["SCROLL", "WRAP", "GRID"], "SCROLL"));
|
|
140
|
+
const navVariant = computed(() => pick(s.value.NAV_STYLE, ["TEXT", "PILL", "IMAGE", "CARD"], "TEXT"));
|
|
141
|
+
const navProps = computed(() => ({
|
|
142
|
+
items: navItems.value,
|
|
143
|
+
variant: navVariant.value,
|
|
144
|
+
shape: pick(s.value.NAV_IMAGE_SHAPE, ["CIRCLE", "SQUARE", "ROUNDED", "PORTRAIT", "LANDSCAPE"], "CIRCLE"),
|
|
145
|
+
textPosition: pick(
|
|
146
|
+
s.value.NAV_TEXT_POSITION,
|
|
147
|
+
["BELOW", "BESIDE", "OVERLAY_CENTER", "OVERLAY_BOTTOM", "HIDDEN"],
|
|
148
|
+
"BELOW"
|
|
149
|
+
),
|
|
150
|
+
activeStyle: pick(s.value.NAV_ACTIVE_STYLE, ["UNDERLINE", "BOLD", "FILL", "BORDER", "NONE"], "UNDERLINE"),
|
|
151
|
+
hover: pick(s.value.NAV_HOVER, ["NONE", "ZOOM", "LIFT"], "ZOOM"),
|
|
152
|
+
imageFit: pick(s.value.NAV_IMAGE_FIT, ["COVER", "CONTAIN"], "COVER"),
|
|
153
|
+
showCount: bool(s.value.NAV_SHOW_COUNT, false),
|
|
154
|
+
label: kind.value === "COLLECTION" ? "Collections" : "Categories",
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
// filters / sort
|
|
158
|
+
const showFilters = computed(() => bool(s.value.SHOW_FILTERS, true));
|
|
159
|
+
const showSort = computed(() => bool(s.value.SHOW_SORT, true));
|
|
160
|
+
const filterPosition = computed(() => {
|
|
161
|
+
const p = pick(s.value.FILTER_POSITION, ["DRAWER", "SIDEBAR"], hasSidebarLayout.value ? "SIDEBAR" : "DRAWER");
|
|
162
|
+
return p === "SIDEBAR" && !hasSidebarLayout.value ? "DRAWER" : p;
|
|
163
|
+
});
|
|
164
|
+
const sortInToolbar = computed(
|
|
165
|
+
() => showSort.value && pick(s.value.SORT_POSITION, ["TOOLBAR", "FILTERS"], "FILTERS") === "TOOLBAR"
|
|
166
|
+
);
|
|
167
|
+
const sortInFilters = computed(() => showSort.value && !sortInToolbar.value);
|
|
168
|
+
/** In the drawer: its own setting, plus the toolbar's sort on phones. */
|
|
169
|
+
const sortInDrawer = computed(() => sortInFilters.value || (sortInToolbar.value && isNarrow.value));
|
|
170
|
+
const swatchShape = computed(() => pick(s.value.SWATCH_SHAPE, ["CIRCLE", "SQUARE"], "CIRCLE"));
|
|
171
|
+
const showActiveFilters = computed(() => bool(s.value.SHOW_ACTIVE_FILTERS, true));
|
|
172
|
+
const drawerSide = computed(() => pick(s.value.DRAWER_SIDE, ["START", "END"], "END"));
|
|
173
|
+
|
|
174
|
+
// sidebar contents
|
|
175
|
+
const sidebarHasNav = computed(() => navPosition.value === "SIDEBAR" && navItems.value.length > 0);
|
|
176
|
+
|
|
177
|
+
// For the header blocks (ClassificationPart).
|
|
178
|
+
provide("osiPlp", { kind, terms, title, description, image, breadcrumbs, navItems, navProps, navLayout });
|
|
179
|
+
const sidebarHasFilters = computed(
|
|
180
|
+
() => filterPosition.value === "SIDEBAR" && showFilters.value && (filters.value.length > 0 || sortInFilters.value)
|
|
181
|
+
);
|
|
182
|
+
const showSidebar = computed(() => hasSidebarLayout.value && (sidebarHasNav.value || sidebarHasFilters.value));
|
|
183
|
+
|
|
184
|
+
// toolbar
|
|
185
|
+
const showToolbar = computed(() => bool(s.value.SHOW_TOOLBAR, true));
|
|
186
|
+
const toolbarFullWidth = computed(() => bool(s.value.TOOLBAR_FULL_WIDTH, true));
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Phones: the toolbar has room for one control, so the sort dropdown moves into
|
|
190
|
+
* the filter panel there, next to the filters. Tracked with matchMedia rather
|
|
191
|
+
* than CSS alone because the panel's contents depend on it.
|
|
192
|
+
*/
|
|
193
|
+
const isNarrow = ref(false);
|
|
194
|
+
let narrowQuery = null;
|
|
195
|
+
const onNarrowChange = (e) => {
|
|
196
|
+
isNarrow.value = e.matches;
|
|
197
|
+
};
|
|
198
|
+
onMounted(() => {
|
|
199
|
+
narrowQuery = window.matchMedia("(max-width: 989px)");
|
|
200
|
+
isNarrow.value = narrowQuery.matches;
|
|
201
|
+
narrowQuery.addEventListener?.("change", onNarrowChange);
|
|
202
|
+
});
|
|
203
|
+
onBeforeUnmount(() => narrowQuery?.removeEventListener?.("change", onNarrowChange));
|
|
204
|
+
const toolbarStyle = computed(() => pick(s.value.TOOLBAR_STYLE, ["BAR", "LINES", "PLAIN"], "BAR"));
|
|
205
|
+
const showCount = computed(() => bool(s.value.SHOW_PRODUCT_COUNT, true));
|
|
206
|
+
const showColumnSwitcher = computed(() => bool(s.value.SHOW_COLUMN_SWITCHER, false));
|
|
207
|
+
/** The drawer button exists on desktop only when filters live in the drawer. */
|
|
208
|
+
const desktopFilterButton = computed(
|
|
209
|
+
() => showFilters.value && filterPosition.value === "DRAWER" && (filters.value.length > 0 || sortInFilters.value)
|
|
210
|
+
);
|
|
211
|
+
/** On mobile the sidebar — and the toolbar's sort — collapse into the drawer. */
|
|
212
|
+
const mobileFilterButton = computed(
|
|
213
|
+
() => desktopFilterButton.value || showSidebar.value || sortInToolbar.value
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* What goes where in the toolbar. Each control can be put at the start, the
|
|
218
|
+
* middle or the end; the product count can also be hidden.
|
|
219
|
+
*/
|
|
220
|
+
const pos = (value, allowed, fallback) =>
|
|
221
|
+
allowed.includes(String(value ?? "").toUpperCase()) ? String(value).toUpperCase() : fallback;
|
|
222
|
+
|
|
223
|
+
const toolbarSlots = computed(() => {
|
|
224
|
+
const slots = { START: [], CENTER: [], END: [] };
|
|
225
|
+
if (mobileFilterButton.value) {
|
|
226
|
+
slots[pos(s.value.TOOLBAR_FILTER_POSITION, ["START", "CENTER", "END"], "START")].push("filter");
|
|
227
|
+
}
|
|
228
|
+
const countAt = pos(s.value.TOOLBAR_COUNT_POSITION, ["START", "CENTER", "END", "HIDDEN"], "START");
|
|
229
|
+
if (showCount.value && countAt !== "HIDDEN") slots[countAt].push("count");
|
|
230
|
+
if (sortInToolbar.value) {
|
|
231
|
+
slots[pos(s.value.TOOLBAR_SORT_POSITION, ["START", "CENTER", "END"], "END")].push("sort");
|
|
232
|
+
}
|
|
233
|
+
if (showColumnSwitcher.value) slots.END.push("columns");
|
|
234
|
+
return slots;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// products
|
|
238
|
+
const columns = computed(() => Math.min(6, Math.max(1, num(s.value.COLUMNS, 4))));
|
|
239
|
+
const userColumns = ref(null);
|
|
240
|
+
const effectiveColumns = computed(() => userColumns.value ?? columns.value);
|
|
241
|
+
const columnChoices = computed(() => [...new Set([2, 3, 4, columns.value])].filter((c) => c <= 6).sort());
|
|
242
|
+
const animate = computed(() => bool(s.value.ANIMATE_PRODUCTS, true));
|
|
243
|
+
const showProgress = computed(() => bool(s.value.SHOW_PROGRESS, false));
|
|
244
|
+
|
|
245
|
+
const cardSettings = computed(() => {
|
|
246
|
+
const out = {};
|
|
247
|
+
for (const key of ["CARD_IMAGE_RATIO", "CARD_TEXT_ALIGN", "CARD_CTA_STYLE", "CARD_TEXT_POSITION"]) {
|
|
248
|
+
if (s.value[key]) out[key] = s.value[key];
|
|
249
|
+
}
|
|
250
|
+
if (s.value.CARD_SHOW_QUICK_SHOP !== undefined && s.value.CARD_SHOW_QUICK_SHOP !== null && s.value.CARD_SHOW_QUICK_SHOP !== "") {
|
|
251
|
+
out.CARD_SHOW_QUICK_SHOP = bool(s.value.CARD_SHOW_QUICK_SHOP, true);
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
/* ------------------------------------------------------------ css vars */
|
|
257
|
+
|
|
258
|
+
const px = (v) => (v === undefined || v === null || v === "" ? undefined : `${Number(v)}px`);
|
|
259
|
+
const compact = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined && v !== null && v !== ""));
|
|
260
|
+
|
|
261
|
+
// The toolbar's space above (schema default): 48 under the category header,
|
|
262
|
+
// 0 at the top of the search results.
|
|
263
|
+
const tbTop = computed(() => (isResults.value ? 0 : 48));
|
|
264
|
+
const tbBottom = computed(() => 0);
|
|
265
|
+
|
|
266
|
+
const cssVars = computed(() => ({
|
|
267
|
+
...compact({
|
|
268
|
+
// header
|
|
269
|
+
"--plp-align": align.value,
|
|
270
|
+
"--plp-title-fs": px(num(s.value.TITLE_SIZE, 40)),
|
|
271
|
+
"--plp-title-fs-mobile": px(num(s.value.TITLE_SIZE_MOBILE, 30)),
|
|
272
|
+
"--plp-title-fw": weight(s.value.TITLE_WEIGHT, 400),
|
|
273
|
+
"--plp-title-tt": CASES[s.value.TITLE_TRANSFORM],
|
|
274
|
+
"--plp-title-color": s.value.TITLE_COLOR,
|
|
275
|
+
"--plp-banner-h": px(num(s.value.BANNER_HEIGHT, 320)),
|
|
276
|
+
"--plp-banner-h-mobile": px(num(s.value.BANNER_HEIGHT_MOBILE, 220)),
|
|
277
|
+
"--plp-banner-overlay": num(s.value.BANNER_OVERLAY, 35) / 100,
|
|
278
|
+
"--plp-banner-color": s.value.BANNER_TEXT_COLOR,
|
|
279
|
+
"--plp-banner-radius": px(s.value.BANNER_RADIUS),
|
|
280
|
+
"--plp-header-gap": px(num(s.value.HEADER_SPACING, 24)),
|
|
281
|
+
|
|
282
|
+
// nav
|
|
283
|
+
"--plp-nav-space": px(num(s.value.NAV_SPACING, 24)),
|
|
284
|
+
// Separate top / bottom, each falling back to the single spacing value.
|
|
285
|
+
"--plp-nav-mt": px(s.value.NAV_MARGIN_TOP),
|
|
286
|
+
"--plp-nav-mb": px(s.value.NAV_MARGIN_BOTTOM),
|
|
287
|
+
"--cnav-img": px(num(s.value.NAV_IMAGE_SIZE, 96)),
|
|
288
|
+
"--cnav-img-mobile": px(s.value.NAV_IMAGE_SIZE_MOBILE),
|
|
289
|
+
"--cnav-gap": px(num(s.value.NAV_GAP, navVariant.value === "TEXT" ? 28 : 20)),
|
|
290
|
+
"--cnav-gap-mobile": px(s.value.NAV_GAP_MOBILE),
|
|
291
|
+
"--cnav-cols": num(s.value.NAV_COLUMNS, 6),
|
|
292
|
+
"--cnav-cols-mobile": num(s.value.NAV_COLUMNS_MOBILE, 3),
|
|
293
|
+
"--cnav-radius": px(num(s.value.NAV_CORNER_RADIUS, 12)),
|
|
294
|
+
"--cnav-overlay": num(s.value.NAV_OVERLAY_OPACITY, 30) / 100,
|
|
295
|
+
"--cnav-justify": JUSTIFY[navAlign.value],
|
|
296
|
+
// Scrolling rows centre with auto margins, which collapse when they overflow.
|
|
297
|
+
"--cnav-scroll-start": navAlign.value === "LEFT" ? "0" : "auto",
|
|
298
|
+
"--cnav-scroll-end": navAlign.value === "CENTER" ? "auto" : "0",
|
|
299
|
+
"--cnav-fs": px(num(s.value.NAV_TEXT_SIZE, 14)),
|
|
300
|
+
"--cnav-fw": weight(s.value.NAV_TEXT_WEIGHT, undefined),
|
|
301
|
+
"--cnav-tt": CASES[s.value.NAV_TEXT_TRANSFORM],
|
|
302
|
+
"--cnav-ls": s.value.NAV_LETTER_SPACING ? `${num(s.value.NAV_LETTER_SPACING, 0) / 100}em` : undefined,
|
|
303
|
+
"--cnav-color": s.value.NAV_TEXT_COLOR,
|
|
304
|
+
"--cnav-active": s.value.NAV_ACTIVE_COLOR,
|
|
305
|
+
"--cnav-card-bg": s.value.NAV_CARD_BG,
|
|
306
|
+
"--cnav-overlay-color": s.value.NAV_OVERLAY_TEXT_COLOR,
|
|
307
|
+
|
|
308
|
+
// toolbar
|
|
309
|
+
"--plp-tb-bg": s.value.TOOLBAR_BG_COLOR,
|
|
310
|
+
"--plp-tb-border": s.value.TOOLBAR_BORDER_COLOR,
|
|
311
|
+
"--plp-tb-color": s.value.TOOLBAR_TEXT_COLOR,
|
|
312
|
+
"--plp-tb-h": px(s.value.TOOLBAR_HEIGHT),
|
|
313
|
+
"--plp-tb-fs": px(s.value.TOOLBAR_TEXT_SIZE),
|
|
314
|
+
// Block layout: the toolbar's and the grid's own spacing (mobile: empty =
|
|
315
|
+
// desktop). The header blocks get theirs from Groups.
|
|
316
|
+
...(flowMode.value
|
|
317
|
+
? {
|
|
318
|
+
"--plp-tb-mt": px(num(s.value.TOOLBAR_MARGIN_TOP, tbTop.value)),
|
|
319
|
+
"--plp-tb-mb": px(num(s.value.TOOLBAR_MARGIN_BOTTOM, tbBottom.value)),
|
|
320
|
+
"--plp-tb-mt-m": px(num(s.value.TOOLBAR_MARGIN_TOP_MOBILE, num(s.value.TOOLBAR_MARGIN_TOP, tbTop.value))),
|
|
321
|
+
"--plp-tb-mb-m": px(num(s.value.TOOLBAR_MARGIN_BOTTOM_MOBILE, num(s.value.TOOLBAR_MARGIN_BOTTOM, tbBottom.value))),
|
|
322
|
+
"--plp-grid-pt": px(num(s.value.GRID_PADDING_TOP, 24)),
|
|
323
|
+
"--plp-grid-pb": px(num(s.value.GRID_PADDING_BOTTOM, 0)),
|
|
324
|
+
"--plp-grid-pt-m": px(num(s.value.GRID_PADDING_TOP_MOBILE, num(s.value.GRID_PADDING_TOP, 24))),
|
|
325
|
+
"--plp-grid-pb-m": px(num(s.value.GRID_PADDING_BOTTOM_MOBILE, num(s.value.GRID_PADDING_BOTTOM, 0))),
|
|
326
|
+
}
|
|
327
|
+
: {}),
|
|
328
|
+
|
|
329
|
+
// layout
|
|
330
|
+
"--plp-sidebar-w": px(num(s.value.SIDEBAR_WIDTH, 260)),
|
|
331
|
+
"--plp-sidebar-gap": px(num(s.value.SIDEBAR_GAP, 40)),
|
|
332
|
+
|
|
333
|
+
// filters
|
|
334
|
+
"--pf-swatch": px(num(s.value.SWATCH_SIZE, 28)),
|
|
335
|
+
|
|
336
|
+
// grid
|
|
337
|
+
"--plp-cols": effectiveColumns.value,
|
|
338
|
+
"--plp-cols-tablet": Math.min(effectiveColumns.value, num(s.value.COLUMNS_TABLET, 3)),
|
|
339
|
+
"--plp-cols-mobile": Math.min(2, Math.max(1, num(s.value.COLUMNS_MOBILE, 2))),
|
|
340
|
+
"--plp-gap-x": px(num(s.value.GRID_GAP_X, 16)),
|
|
341
|
+
"--plp-gap-y": px(num(s.value.GRID_GAP_Y, 32)),
|
|
342
|
+
"--plp-gap-mobile": px(num(s.value.GRID_GAP_MOBILE, 8)),
|
|
343
|
+
}),
|
|
344
|
+
}));
|
|
345
|
+
|
|
346
|
+
const rootStyle = computed(() => ({
|
|
347
|
+
...shellStyle.value,
|
|
348
|
+
...cssVars.value,
|
|
349
|
+
"--plp-inner-max": innerStyle.value.maxWidth,
|
|
350
|
+
}));
|
|
351
|
+
|
|
352
|
+
/** The teleported drawer needs the colours and vars, not the section's box. */
|
|
353
|
+
const drawerStyle = computed(() => ({ ...scheme.value.style, ...cssVars.value }));
|
|
354
|
+
|
|
355
|
+
const rootClasses = computed(() => [
|
|
356
|
+
customClass.value,
|
|
357
|
+
`osi-plp--layout-${pageLayout.value.toLowerCase().replace("_", "-")}`,
|
|
358
|
+
`osi-plp--toolbar-${toolbarStyle.value.toLowerCase()}`,
|
|
359
|
+
`osi-plp--align-${align.value}`,
|
|
360
|
+
{
|
|
361
|
+
"osi-plp--toolbar-bleed": toolbarFullWidth.value,
|
|
362
|
+
"osi-plp--toolbar-sticky": bool(s.value.TOOLBAR_STICKY, false),
|
|
363
|
+
"osi-plp--sidebar-sticky": bool(s.value.SIDEBAR_STICKY, true),
|
|
364
|
+
"osi-plp--animate": animate.value,
|
|
365
|
+
},
|
|
366
|
+
]);
|
|
367
|
+
|
|
368
|
+
/* ------------------------------------------------------------ labels */
|
|
369
|
+
|
|
370
|
+
/** "{terms}" in a label: what was searched (search page). */
|
|
371
|
+
const withTerms = (text) => String(text ?? "").replaceAll("{terms}", terms.value);
|
|
372
|
+
|
|
373
|
+
const L = computed(() => ({
|
|
374
|
+
filter: t(s.value.FILTER_BUTTON_LABEL) || (showSort.value ? "Filter & sort" : "Filter"),
|
|
375
|
+
filters: t(s.value.FILTER_TITLE) || "Filters",
|
|
376
|
+
sortBy: t(s.value.SORT_LABEL) || "Sort by",
|
|
377
|
+
apply: t(s.value.APPLY_LABEL) || "Apply",
|
|
378
|
+
clear: t(s.value.CLEAR_LABEL) || "Clear all",
|
|
379
|
+
loadMore: t(s.value.LOAD_MORE_LABEL) || "Load more",
|
|
380
|
+
emptyTitle: withTerms(t(s.value.EMPTY_TITLE)) || "No products found",
|
|
381
|
+
emptyText: withTerms(t(s.value.EMPTY_TEXT)) || "",
|
|
382
|
+
product: t(s.value.COUNT_SINGULAR) || "product",
|
|
383
|
+
products: t(s.value.COUNT_PLURAL) || "products",
|
|
384
|
+
}));
|
|
385
|
+
|
|
386
|
+
const countLabel = computed(() => `${total.value} ${total.value === 1 ? L.value.product : L.value.products}`);
|
|
387
|
+
|
|
388
|
+
/* --------------------------------------------------------------- drawer */
|
|
389
|
+
|
|
390
|
+
const drawerOpen = ref(false);
|
|
391
|
+
const draftValues = ref([]);
|
|
392
|
+
const draftOrdering = ref("");
|
|
393
|
+
useBodyScrollLock(drawerOpen);
|
|
394
|
+
|
|
395
|
+
function openDrawer() {
|
|
396
|
+
draftValues.value = [...selectedValues.value];
|
|
397
|
+
draftOrdering.value = ordering.value;
|
|
398
|
+
drawerOpen.value = true;
|
|
399
|
+
}
|
|
400
|
+
function closeDrawer() {
|
|
401
|
+
drawerOpen.value = false;
|
|
402
|
+
}
|
|
403
|
+
function toggleDraft(id) {
|
|
404
|
+
const set = new Set(draftValues.value);
|
|
405
|
+
set.has(id) ? set.delete(id) : set.add(id);
|
|
406
|
+
draftValues.value = [...set];
|
|
407
|
+
}
|
|
408
|
+
async function applyDrawer() {
|
|
409
|
+
await page.setValues(draftValues.value);
|
|
410
|
+
if (draftOrdering.value !== ordering.value) await page.setOrdering(draftOrdering.value);
|
|
411
|
+
closeDrawer();
|
|
412
|
+
}
|
|
413
|
+
async function clearDrawer() {
|
|
414
|
+
draftValues.value = [];
|
|
415
|
+
draftOrdering.value = "";
|
|
416
|
+
await page.clearFilters();
|
|
417
|
+
closeDrawer();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function onKeydown(e) {
|
|
421
|
+
if (e.key === "Escape" && drawerOpen.value) closeDrawer();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* ------------------------------------------------------------ pagination */
|
|
425
|
+
|
|
426
|
+
const root = ref(null);
|
|
427
|
+
const sentinel = ref(null);
|
|
428
|
+
let observer = null;
|
|
429
|
+
|
|
430
|
+
function setupObserver() {
|
|
431
|
+
observer?.disconnect();
|
|
432
|
+
observer = null;
|
|
433
|
+
if (pagination.value !== "INFINITE" || !sentinel.value || typeof IntersectionObserver === "undefined") return;
|
|
434
|
+
observer = new IntersectionObserver(
|
|
435
|
+
(entries) => {
|
|
436
|
+
if (entries.some((e) => e.isIntersecting)) page.loadMore();
|
|
437
|
+
},
|
|
438
|
+
{ rootMargin: "800px 0px" }
|
|
439
|
+
);
|
|
440
|
+
observer.observe(sentinel.value);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
watch([sentinel, pagination], setupObserver);
|
|
444
|
+
|
|
445
|
+
const pageWindow = computed(() => {
|
|
446
|
+
const current = page.page.value;
|
|
447
|
+
const last = pageCount.value;
|
|
448
|
+
const out = new Set([1, last, current, current - 1, current + 1]);
|
|
449
|
+
const list = [...out].filter((n) => n >= 1 && n <= last).sort((a, b) => a - b);
|
|
450
|
+
const withGaps = [];
|
|
451
|
+
list.forEach((n, i) => {
|
|
452
|
+
if (i && n - list[i - 1] > 1) withGaps.push(`gap-${n}`);
|
|
453
|
+
withGaps.push(n);
|
|
454
|
+
});
|
|
455
|
+
return withGaps;
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
async function goToPage(n) {
|
|
459
|
+
await page.goToPage(n);
|
|
460
|
+
root.value?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
onMounted(() => {
|
|
464
|
+
window.addEventListener("keydown", onKeydown);
|
|
465
|
+
setupObserver();
|
|
466
|
+
});
|
|
467
|
+
onBeforeUnmount(() => {
|
|
468
|
+
window.removeEventListener("keydown", onKeydown);
|
|
469
|
+
observer?.disconnect();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
return { LISTING_BLOCKS, effectiveSection, s, t, shellStyle, innerStyle, align, customClass, scheme, page, kind, terms, title, description, image, breadcrumbs, navItems, sortOptions, ordering, selectedValues, filters, filtersPending, activeFilters, products, total, pageCount, pagination, pending, hasMore, loadingMore, blockMode, isResults, flowMode, PART, tree, flow, orderOf, isPreview, awaitingTerms, toolbarOrder, gridOrder, blockChunks, cardTemplate, cardItems, itemsById, num, bool, pick, WEIGHTS, CASES, weight, JUSTIFY, showBreadcrumbs, showTitle, showDescription, bannerStyle, wantsSidebar, pageLayout, hasSidebarLayout, navPosition, navAlign, navLayout, navVariant, navProps, showFilters, showSort, filterPosition, sortInToolbar, sortInFilters, sortInDrawer, swatchShape, showActiveFilters, drawerSide, sidebarHasNav, sidebarHasFilters, showSidebar, showToolbar, toolbarFullWidth, isNarrow, narrowQuery, onNarrowChange, toolbarStyle, showCount, showColumnSwitcher, desktopFilterButton, mobileFilterButton, pos, toolbarSlots, columns, userColumns, effectiveColumns, columnChoices, animate, showProgress, cardSettings, px, compact, tbTop, tbBottom, cssVars, rootStyle, drawerStyle, rootClasses, withTerms, L, countLabel, drawerOpen, draftValues, draftOrdering, openDrawer, closeDrawer, toggleDraft, applyDrawer, clearDrawer, onKeydown, root, sentinel, observer, setupObserver, pageWindow, goToPage };
|
|
473
|
+
}
|