@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,91 @@
|
|
|
1
|
+
|
|
2
|
+
export default function useLogic({ props }) {
|
|
3
|
+
const t = useT();
|
|
4
|
+
const { store, storeLogo, menu: menuByHandle } = useStorefront();
|
|
5
|
+
const { normalizeItems } = useMenu();
|
|
6
|
+
const { mediaUrl } = useMediaUrl();
|
|
7
|
+
const { isMobile, isPreview } = { ...useLayoutShell(), ...useBuilderPreview() };
|
|
8
|
+
const brand = useThemeGroup("BRAND");
|
|
9
|
+
|
|
10
|
+
const s = computed(() => props.node.s ?? {});
|
|
11
|
+
const type = computed(() => props.node.type);
|
|
12
|
+
const px = (v, d) => {
|
|
13
|
+
const n = Number(v);
|
|
14
|
+
return v === "" || v == null || !Number.isFinite(n) ? d : `${n}px`;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/* ------------------------------------------------------------------ menu */
|
|
18
|
+
|
|
19
|
+
const menu = computed(() => {
|
|
20
|
+
const found = s.value.MENU ? menuByHandle(s.value.MENU) : null;
|
|
21
|
+
return { title: t(found?.title) || "", items: normalizeItems(found?.items ?? []) };
|
|
22
|
+
});
|
|
23
|
+
const heading = computed(() =>
|
|
24
|
+
s.value.SHOW_HEADING === false ? "" : t(s.value.HEADING) || menu.value.title
|
|
25
|
+
);
|
|
26
|
+
const row = computed(() => String(s.value.DIRECTION || "VERTICAL").toUpperCase() === "HORIZONTAL");
|
|
27
|
+
const collapsible = computed(() => !row.value && !!s.value.COLLAPSE_MOBILE && !!heading.value);
|
|
28
|
+
const open = ref(false);
|
|
29
|
+
const collapsed = computed(() => collapsible.value && isMobile.value && !open.value);
|
|
30
|
+
|
|
31
|
+
const WEIGHTS = { LIGHT: 300, REGULAR: 400, MEDIUM: 500, SEMIBOLD: 600, BOLD: 700 };
|
|
32
|
+
const weightOf = (v) => {
|
|
33
|
+
const n = Number(v);
|
|
34
|
+
return Number.isFinite(n) && n > 0 ? n : WEIGHTS[String(v || "").toUpperCase()];
|
|
35
|
+
};
|
|
36
|
+
const CASES = { NONE: "none", UPPERCASE: "uppercase", LOWERCASE: "lowercase", CAPITALIZE: "capitalize" };
|
|
37
|
+
const textVars = (prefix) => {
|
|
38
|
+
const v = s.value;
|
|
39
|
+
return {
|
|
40
|
+
[`--fm-${prefix}-fs`]: px(v[`${prefix.toUpperCase()}_SIZE`], undefined),
|
|
41
|
+
[`--fm-${prefix}-fs-m`]: px(v[`${prefix.toUpperCase()}_SIZE_MOBILE`], undefined),
|
|
42
|
+
[`--fm-${prefix}-fw`]: weightOf(v[`${prefix.toUpperCase()}_WEIGHT`]),
|
|
43
|
+
[`--fm-${prefix}-color`]: v[`${prefix.toUpperCase()}_COLOR`] || undefined,
|
|
44
|
+
[`--fm-${prefix}-lh`]: v[`${prefix.toUpperCase()}_LINE_HEIGHT`]
|
|
45
|
+
? String(Number(v[`${prefix.toUpperCase()}_LINE_HEIGHT`]) / 100)
|
|
46
|
+
: undefined,
|
|
47
|
+
[`--fm-${prefix}-ls`]: v[`${prefix.toUpperCase()}_LETTER_SPACING`] != null
|
|
48
|
+
? `${Number(v[`${prefix.toUpperCase()}_LETTER_SPACING`]) / 100}em`
|
|
49
|
+
: undefined,
|
|
50
|
+
[`--fm-${prefix}-ff`]:
|
|
51
|
+
String(v[`${prefix.toUpperCase()}_FONT`] || "").toUpperCase() === "HEADING"
|
|
52
|
+
? "var(--osi-font-heading)"
|
|
53
|
+
: undefined,
|
|
54
|
+
[`--fm-${prefix}-tt`]: CASES[String(v[`${prefix.toUpperCase()}_TRANSFORM`] || "").toUpperCase()],
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
const menuVars = computed(() => {
|
|
58
|
+
const v = s.value;
|
|
59
|
+
const out = {
|
|
60
|
+
"--fm-gap": px(v.LINK_GAP, "12px"),
|
|
61
|
+
"--fm-gap-m": px(v.LINK_GAP_MOBILE, px(v.LINK_GAP, "12px")),
|
|
62
|
+
"--fm-heading-gap": px(v.HEADING_GAP, "14px"),
|
|
63
|
+
"--fm-align": { CENTER: "center", RIGHT: "end" }[String(v.TEXT_ALIGN || "").toUpperCase()] ?? "start",
|
|
64
|
+
"--fm-hover": v.LINK_HOVER_COLOR || undefined,
|
|
65
|
+
...textVars("heading"),
|
|
66
|
+
...textVars("link"),
|
|
67
|
+
};
|
|
68
|
+
return Object.fromEntries(Object.entries(out).filter(([, x]) => x !== undefined));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/* ------------------------------------------------------------------ logo */
|
|
72
|
+
|
|
73
|
+
const logoSrc = computed(() =>
|
|
74
|
+
mediaUrl(s.value.LOGO?.path ?? s.value.LOGO ?? "") || mediaUrl(brand.value.LOGO || storeLogo.value)
|
|
75
|
+
);
|
|
76
|
+
const logoVars = computed(() => ({
|
|
77
|
+
"--fl-w": px(s.value.LOGO_WIDTH, "120px"),
|
|
78
|
+
"--fl-w-m": px(s.value.LOGO_WIDTH_MOBILE, px(s.value.LOGO_WIDTH, "120px")),
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
/* ------------------------------------------------------------ newsletter */
|
|
82
|
+
|
|
83
|
+
const submitted = ref(false);
|
|
84
|
+
const email = ref("");
|
|
85
|
+
function subscribe() {
|
|
86
|
+
if (isPreview.value) return; // the editor never signs anyone up
|
|
87
|
+
submitted.value = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { t, store, storeLogo, menuByHandle, normalizeItems, mediaUrl, isMobile, isPreview, brand, s, type, px, menu, heading, row, collapsible, open, collapsed, WEIGHTS, weightOf, CASES, textVars, menuVars, logoSrc, logoVars, submitted, email, subscribe };
|
|
91
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The footer's social links, rendered once.
|
|
3
|
+
*
|
|
4
|
+
* SiteFooter used to carry three copies of this markup — one per placement —
|
|
5
|
+
* so every fix had to be made three times and they had already drifted (only
|
|
6
|
+
* one of them applied the alignment style).
|
|
7
|
+
*
|
|
8
|
+
* Placement stays SiteFooter's decision; this component only knows how to draw
|
|
9
|
+
* the list. `align` is passed rather than read from settings because the
|
|
10
|
+
* bottom-bar zone is aligned by its grid cell, not by FOOTER_SOCIAL_ALIGNMENT.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
export default function useLogic({ props }) {
|
|
15
|
+
const { socials, showIcon, showText, iconStyle, iconBackground, iconSize, iconColor } =
|
|
16
|
+
useFooterSocials();
|
|
17
|
+
|
|
18
|
+
const SOCIAL_ICON_PATHS = {
|
|
19
|
+
instagram:
|
|
20
|
+
"M7 2h10a5 5 0 0 1 5 5v10a5 5 0 0 1-5 5H7a5 5 0 0 1-5-5V7a5 5 0 0 1 5-5Zm5 5.5A4.5 4.5 0 1 0 16.5 12 4.5 4.5 0 0 0 12 7.5Zm0 2A2.5 2.5 0 1 1 9.5 12 2.5 2.5 0 0 1 12 9.5ZM17.25 6a1 1 0 1 0 1 1 1 1 0 0 0-1-1Z",
|
|
21
|
+
facebook:
|
|
22
|
+
"M13.5 22v-8.5H16l.5-3.5h-3V7.8c0-1 .3-1.8 1.8-1.8H16.6V2.8A22 22 0 0 0 14 2.5c-2.6 0-4.5 1.6-4.5 4.6V10H7v3.5h2.5V22Z",
|
|
23
|
+
tiktok:
|
|
24
|
+
"M14.5 2h3a5.5 5.5 0 0 0 4.2 4.3v3.1a8.6 8.6 0 0 1-4.2-1.4v6.9a6.6 6.6 0 1 1-6.6-6.6c.3 0 .6 0 .9.1v3.2a3.4 3.4 0 1 0 2.4 3.3V2Z",
|
|
25
|
+
twitter:
|
|
26
|
+
"M21 5.9a8 8 0 0 1-2.3.6 4 4 0 0 0 1.8-2.2 8 8 0 0 1-2.5 1 4 4 0 0 0-6.9 3.6A11.4 11.4 0 0 1 3 4.9a4 4 0 0 0 1.2 5.3 4 4 0 0 1-1.8-.5v.1a4 4 0 0 0 3.2 3.9 4 4 0 0 1-1.8.1 4 4 0 0 0 3.7 2.8A8 8 0 0 1 2 18.4a11.3 11.3 0 0 0 6.1 1.8c7.3 0 11.3-6.1 11.3-11.3v-.5A8.2 8.2 0 0 0 21 5.9Z",
|
|
27
|
+
youtube:
|
|
28
|
+
"M21.6 7.6a3 3 0 0 0-2.1-2.1C17.7 5 12 5 12 5s-5.7 0-7.5.5A3 3 0 0 0 2.4 7.6 31 31 0 0 0 2 12a31 31 0 0 0 .4 4.4 3 3 0 0 0 2.1 2.1C6.3 19 12 19 12 19s5.7 0 7.5-.5a3 3 0 0 0 2.1-2.1A31 31 0 0 0 22 12a31 31 0 0 0-.4-4.4ZM10 15V9l5.2 3Z",
|
|
29
|
+
pinterest:
|
|
30
|
+
"M12 2a10 10 0 0 0-3.6 19.3c0-.8 0-1.7.2-2.5l1.4-6s-.4-.7-.4-1.8c0-1.7 1-3 2.2-3 1 0 1.5.8 1.5 1.7 0 1-.7 2.6-1 4-.3 1.2.6 2.2 1.8 2.2 2.1 0 3.7-2.3 3.7-5.5 0-2.9-2-4.9-5-4.9a5.2 5.2 0 0 0-5.4 5.2c0 1 .4 2.1 1 2.7l-1 3.9A10 10 0 0 0 12 2Z",
|
|
31
|
+
linkedin:
|
|
32
|
+
"M6.9 8.4H3.3V21h3.6Zm-1.8-5.7a2.1 2.1 0 1 0 0 4.2 2.1 2.1 0 0 0 0-4.2ZM21 21v-6.9c0-3.7-2-5.4-4.6-5.4A4 4 0 0 0 12.8 10.7V8.4H9.2c0 .3 0 5.4 0 12.6h3.6v-7c0-.4 0-.7.1-1a2.1 2.1 0 0 1 2-1.4c1.4 0 2 1.1 2 2.6V21Z",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const SOCIAL_ICON_FALLBACK =
|
|
36
|
+
"M3.9 12a4.6 4.6 0 0 1 4.6-4.6h3v1.8h-3a2.8 2.8 0 1 0 0 5.6h3V16.6h-3A4.6 4.6 0 0 1 3.9 12Zm7-1h6.2v2H10.9ZM16.5 7.4h3a4.6 4.6 0 1 1 0 9.2h-3v-1.8h3a2.8 2.8 0 1 0 0-5.6h-3Z";
|
|
37
|
+
|
|
38
|
+
function iconPathFor(social) {
|
|
39
|
+
const key = (social.platform || social.name || "").toLowerCase();
|
|
40
|
+
return SOCIAL_ICON_PATHS[key] || SOCIAL_ICON_FALLBACK;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function labelFor(social) {
|
|
44
|
+
return social.platform || social.name || "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The circle/square box used to be a hardcoded 2.25rem, so raising
|
|
49
|
+
* FOOTER_SOCIAL_ICON_SIZE past ~36px pushed the glyph outside its own
|
|
50
|
+
* background. The box now scales with the icon.
|
|
51
|
+
*/
|
|
52
|
+
const listStyle = computed(() => ({
|
|
53
|
+
"--osi-social-icon-size": `${iconSize.value}px`,
|
|
54
|
+
"--osi-social-box": `${Math.round(iconSize.value * 2)}px`,
|
|
55
|
+
...(iconColor.value ? { "--osi-social-icon-color": iconColor.value } : {}),
|
|
56
|
+
...(props.align ? { justifyContent: props.align } : {}),
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
return { socials, showIcon, showText, iconStyle, iconBackground, iconSize, iconColor, SOCIAL_ICON_PATHS, SOCIAL_ICON_FALLBACK, iconPathFor, labelFor, listStyle };
|
|
60
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dropdown's rows (HEADER_MENU_SUBMENU_STYLE = DROPDOWN).
|
|
3
|
+
*
|
|
4
|
+
* A row whose item has its own children gets a chevron that expands them in
|
|
5
|
+
* place — an accordion inside the same panel, indented under their parent —
|
|
6
|
+
* rather than a second flyout to the side. Recursive (it renders itself for
|
|
7
|
+
* the children), so every level the menu has is reachable.
|
|
8
|
+
*
|
|
9
|
+
* The row's label stays a real link to the category; only the chevron
|
|
10
|
+
* toggles, so a parent category is still a page you can go to.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
export default function useLogic({ props, emit }) {
|
|
15
|
+
const { isActive } = useNavActive();
|
|
16
|
+
|
|
17
|
+
// A branch holding the current page starts open, so you see where you are.
|
|
18
|
+
function holdsActive(item) {
|
|
19
|
+
return (item.children ?? []).some((c) => isActive(c) || holdsActive(c));
|
|
20
|
+
}
|
|
21
|
+
const expanded = ref(new Set(props.items.filter(holdsActive).map((i) => i.id)));
|
|
22
|
+
function toggle(id) {
|
|
23
|
+
const next = new Set(expanded.value);
|
|
24
|
+
next.has(id) ? next.delete(id) : next.add(id);
|
|
25
|
+
expanded.value = next;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { isActive, holdsActive, expanded, toggle };
|
|
29
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header icons, in three sets.
|
|
3
|
+
*
|
|
4
|
+
* A merchant asking to "change the icons" almost always means picking a style,
|
|
5
|
+
* not uploading their own SVG. So this is an enum, not an upload field.
|
|
6
|
+
*
|
|
7
|
+
* Arbitrary merchant SVG is deliberately not supported here. Rendering it needs
|
|
8
|
+
* v-html, and a `<svg>` accepts `<script>`, `onload`, and `<foreignObject>` —
|
|
9
|
+
* that's stored XSS on a storefront, executing against a logged-in shopper's
|
|
10
|
+
* session. If custom icons become a real requirement, they go through a
|
|
11
|
+
* server-side sanitiser (allow-list of tags and attributes) before they ever
|
|
12
|
+
* reach the bootstrap payload, and this component gains a `custom` branch that
|
|
13
|
+
* reads only from that sanitised field.
|
|
14
|
+
*
|
|
15
|
+
* MINIMAL's two-bar burger and hairline weights are Terazar's set.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
export default function useLogic({ props }) {
|
|
20
|
+
const SETS = {
|
|
21
|
+
LINE: {
|
|
22
|
+
stroke: 1.5,
|
|
23
|
+
fill: "none",
|
|
24
|
+
paths: {
|
|
25
|
+
menu: ["M3.75 6.75h16.5M3.75 12h16.5M3.75 17.25h16.5"],
|
|
26
|
+
search: ["m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"],
|
|
27
|
+
account: [
|
|
28
|
+
"M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z",
|
|
29
|
+
],
|
|
30
|
+
cart: [
|
|
31
|
+
"M15.75 10.5V6a3.75 3.75 0 1 0-7.5 0v4.5m11.356-1.993 1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 0 1-1.12-1.243l1.264-12A1.125 1.125 0 0 1 5.513 7.5h12.974c.576 0 1.059.435 1.119 1.007Z",
|
|
32
|
+
],
|
|
33
|
+
close: ["M6 18 18 6M6 6l12 12"],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
MINIMAL: {
|
|
38
|
+
stroke: 1.1,
|
|
39
|
+
fill: "none",
|
|
40
|
+
paths: {
|
|
41
|
+
menu: ["M5 8.7h14M5 14.9h14"],
|
|
42
|
+
search: ["m20.5 20.5-4.6-4.6m0 0a6.6 6.6 0 1 0-9.3-9.3 6.6 6.6 0 0 0 9.3 9.3Z"],
|
|
43
|
+
account: ["M15 7.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM5.5 19.5a6.5 6.5 0 0 1 13 0"],
|
|
44
|
+
cart: ["M6.5 8h11l1 12.5h-13L6.5 8Zm2.75 0V6.25a2.75 2.75 0 0 1 5.5 0V8"],
|
|
45
|
+
close: ["M6.5 6.5l11 11M17.5 6.5l-11 11"],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
SOLID: {
|
|
50
|
+
stroke: 0,
|
|
51
|
+
fill: "currentColor",
|
|
52
|
+
paths: {
|
|
53
|
+
menu: ["M3 6.75A.75.75 0 0 1 3.75 6h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 6.75Zm0 5.25a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 12Zm0 5.25a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75a.75.75 0 0 1-.75-.75Z"],
|
|
54
|
+
search: ["M10.5 3a7.5 7.5 0 1 0 4.55 13.46l4.24 4.25a.75.75 0 1 0 1.06-1.06l-4.24-4.25A7.5 7.5 0 0 0 10.5 3Zm-6 7.5a6 6 0 1 1 12 0 6 6 0 0 1-12 0Z"],
|
|
55
|
+
account: ["M12 2.25a3.75 3.75 0 1 0 0 7.5 3.75 3.75 0 0 0 0-7.5ZM3.75 20.1a8.25 8.25 0 0 1 16.5 0 .9.9 0 0 1-.48.82A18.7 18.7 0 0 1 12 22.5c-2.79 0-5.43-.61-7.8-1.7a.9.9 0 0 1-.45-.7Z"],
|
|
56
|
+
cart: ["M8.25 6a3.75 3.75 0 0 1 7.5 0v1.5h2.74c.58 0 1.06.43 1.12 1.01l1.26 12A1.13 1.13 0 0 1 19.75 21.75H4.25a1.13 1.13 0 0 1-1.12-1.24l1.26-12A1.13 1.13 0 0 1 5.51 7.5h2.74V6Zm1.5 1.5h4.5V6a2.25 2.25 0 0 0-4.5 0v1.5Z"],
|
|
57
|
+
close: ["M5.47 5.47a.75.75 0 0 1 1.06 0L12 10.94l5.47-5.47a.75.75 0 1 1 1.06 1.06L13.06 12l5.47 5.47a.75.75 0 1 1-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 0 1-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 0 1 0-1.06Z"],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// An unknown set (older theme, renamed enum) falls back rather than rendering
|
|
63
|
+
// an empty box where the cart button should be.
|
|
64
|
+
const active = computed(() => SETS[props.set] ?? SETS.LINE);
|
|
65
|
+
const paths = computed(() => active.value.paths[props.name] ?? []);
|
|
66
|
+
|
|
67
|
+
return { SETS, active, paths };
|
|
68
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The header logo.
|
|
3
|
+
*
|
|
4
|
+
* Extracted for the same reason HeaderNav was: LOGO_LEFT_MENU_CENTER needs the
|
|
5
|
+
* logo in the start cell while every other layout keeps it in the centre cell,
|
|
6
|
+
* and a second copy of this markup in SiteHeader would be a second place to fix
|
|
7
|
+
* every logo bug.
|
|
8
|
+
*
|
|
9
|
+
* Reads BRAND itself rather than taking the resolved src as a prop, so the
|
|
10
|
+
* mobile/desktop swap lives with the thing it describes.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* altSrc / showAlt: a second logo (the transparent header's, e.g. a white
|
|
14
|
+
* one) cross-faded over the main one while showAlt is on. Both are stacked
|
|
15
|
+
* in one spot, so the swap never shifts the header.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
export default function useLogic({ props }) {
|
|
20
|
+
const brand = useThemeGroup("BRAND");
|
|
21
|
+
const { store, storeLogo } = useStorefront();
|
|
22
|
+
const { mediaUrl } = useMediaUrl();
|
|
23
|
+
const { isMobile } = useLayoutShell();
|
|
24
|
+
|
|
25
|
+
const src = computed(() => {
|
|
26
|
+
const desktop = brand.value.LOGO || storeLogo.value;
|
|
27
|
+
const mobile = brand.value.LOGO_MOBILE || desktop;
|
|
28
|
+
return mediaUrl(isMobile.value ? mobile : desktop);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// LOGO_WIDTH_MOBILE: phones often want a smaller logo than desktop.
|
|
32
|
+
const width = computed(() => {
|
|
33
|
+
const mobile = Number(brand.value.LOGO_WIDTH_MOBILE);
|
|
34
|
+
const n = isMobile.value && Number.isFinite(mobile) && mobile > 0 ? mobile : Number(brand.value.LOGO_WIDTH);
|
|
35
|
+
return Number.isFinite(n) && n > 0 ? `${n}px` : null;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return { brand, store, storeLogo, mediaUrl, isMobile, src, width };
|
|
39
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { NuxtLink } from "#components";
|
|
2
|
+
import ProductServices from "#osi/services/ProductServices";
|
|
3
|
+
|
|
4
|
+
export default function useLogic({ props }) {
|
|
5
|
+
const { isActive } = useNavActive();
|
|
6
|
+
|
|
7
|
+
const openId = ref(null);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hover opens after a beat and closes after a longer one.
|
|
11
|
+
*
|
|
12
|
+
* Without the close delay, the 10px gap between the link and the panel is
|
|
13
|
+
* enough to drop the menu while the pointer is travelling to it — the single
|
|
14
|
+
* most common complaint about hover menus. The open delay stops menus flashing
|
|
15
|
+
* open as the pointer sweeps across the bar on its way somewhere else.
|
|
16
|
+
*/
|
|
17
|
+
let openTimer = null;
|
|
18
|
+
let closeTimer = null;
|
|
19
|
+
|
|
20
|
+
function clearTimers() {
|
|
21
|
+
clearTimeout(openTimer);
|
|
22
|
+
clearTimeout(closeTimer);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function open(id, delay = 60) {
|
|
26
|
+
clearTimers();
|
|
27
|
+
openTimer = setTimeout(() => (openId.value = id), delay);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function close(delay = 180) {
|
|
31
|
+
clearTimers();
|
|
32
|
+
closeTimer = setTimeout(() => (openId.value = null), delay);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toggle(id) {
|
|
36
|
+
clearTimers();
|
|
37
|
+
openId.value = openId.value === id ? null : id;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Touch has no hover. Without this the parent link navigates on the first tap
|
|
42
|
+
* and the submenu is unreachable, so the caret is a real button that toggles
|
|
43
|
+
* the panel — which is also what makes the menu operable by keyboard.
|
|
44
|
+
*/
|
|
45
|
+
function onCaret(id) {
|
|
46
|
+
toggle(id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ------------------------------------------------------------- mega menu */
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `<component :is="'NuxtLink'">` doesn't work in Nuxt: components aren't
|
|
53
|
+
* registered globally, so the string resolves to nothing and renders a bare
|
|
54
|
+
* <nuxtlink> tag — which is why the mega menu's images weren't clickable.
|
|
55
|
+
* The component itself has to be resolved.
|
|
56
|
+
*/
|
|
57
|
+
const LinkComponent = NuxtLink;
|
|
58
|
+
|
|
59
|
+
const mega = computed(() => String(props.submenuStyle || "").toUpperCase() === "MEGA");
|
|
60
|
+
const { mediaUrl } = useMediaUrl();
|
|
61
|
+
|
|
62
|
+
const t = useT();
|
|
63
|
+
const { productUrl } = useProductUrl();
|
|
64
|
+
const { store } = useStorefront();
|
|
65
|
+
const up = (v, d = "") => String(v ?? d).toUpperCase();
|
|
66
|
+
const px = (v) => (v === "" || v == null || !Number.isFinite(Number(v)) ? undefined : `${Number(v)}px`);
|
|
67
|
+
const WEIGHTS = { LIGHT: 300, REGULAR: 400, MEDIUM: 500, SEMIBOLD: 600, BOLD: 700 };
|
|
68
|
+
const weightOf = (v) => (Number(v) > 0 ? Number(v) : WEIGHTS[up(v)]);
|
|
69
|
+
const CASES = { NONE: "none", UPPERCASE: "uppercase", LOWERCASE: "lowercase", CAPITALIZE: "capitalize" };
|
|
70
|
+
|
|
71
|
+
/** The Mega menu block for a top-level link (its ITEM text), if any. */
|
|
72
|
+
function overrideFor(item) {
|
|
73
|
+
const title = String(item?.title || "").trim().toLowerCase();
|
|
74
|
+
return props.megaBlocks.find((b) => String(t(b.settings.ITEM) || "").trim().toLowerCase() === title) ?? null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A Mega menu block makes its link a mega menu whatever the Menu block's
|
|
79
|
+
* submenu style (dropdown included) — adding one is asking for it.
|
|
80
|
+
*/
|
|
81
|
+
function megaFor(item) {
|
|
82
|
+
return mega.value || !!overrideFor(item);
|
|
83
|
+
}
|
|
84
|
+
/** Any link opens as a mega menu: the nav becomes the panel's anchor. */
|
|
85
|
+
const anyMega = computed(() => mega.value || props.items.some((i) => overrideFor(i)));
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Whether a link opens anything: its own sub-links, or — for a link with none
|
|
89
|
+
* (e.g. "About") — a Mega menu block featuring products or its own images.
|
|
90
|
+
*/
|
|
91
|
+
function hasPanel(item) {
|
|
92
|
+
if (item.children.length) return true;
|
|
93
|
+
const o = overrideFor(item);
|
|
94
|
+
if (!o) return false;
|
|
95
|
+
const os = o.settings ?? {};
|
|
96
|
+
const source = up(os.FEATURED, "MENU_IMAGES");
|
|
97
|
+
if (source === "PRODUCTS") return (os.PRODUCTS ?? []).length > 0;
|
|
98
|
+
if (source === "CUSTOM") return [1, 2, 3, 4].some((n) => os[`IMAGE_${n}`]);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Products a block features, fetched when its menu first opens. */
|
|
103
|
+
const productCache = reactive({});
|
|
104
|
+
async function loadProducts(block) {
|
|
105
|
+
const ids = (block.settings.PRODUCTS ?? []).map((v) => (v && typeof v === "object" ? v.id : v)).filter(Boolean);
|
|
106
|
+
const key = `${block.key}:${ids.join(",")}`;
|
|
107
|
+
if (!ids.length || productCache[key]) return;
|
|
108
|
+
productCache[key] = { loading: true, rows: [] };
|
|
109
|
+
try {
|
|
110
|
+
const res = await ProductServices.getProductsByIds(ids);
|
|
111
|
+
const rows = res?.results ?? res ?? [];
|
|
112
|
+
const byId = new Map(rows.map((p) => [String(p.id), p]));
|
|
113
|
+
productCache[key] = { loading: false, rows: ids.map((id) => byId.get(String(id))).filter(Boolean) };
|
|
114
|
+
} catch {
|
|
115
|
+
productCache[key] = { loading: false, rows: [] };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const currency = computed(() => store.value?.default_currency?.symbol || store.value?.default_currency?.code || "$");
|
|
119
|
+
function priceOf(p) {
|
|
120
|
+
const n = String(p?.discounted_price_range ?? p?.price_range ?? "").split("-").map(parseFloat).filter((x) => x > 0);
|
|
121
|
+
return n.length ? `${currency.value}${n[0]}` : "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The open menu: its config (the Menu block's mega settings, overridden by
|
|
126
|
+
* its Mega menu block), its link columns — a sub-item with its own links is a
|
|
127
|
+
* column (heading + links), plain sub-items share one — and its featured
|
|
128
|
+
* cards: the categories' images, picked products, or custom images.
|
|
129
|
+
*/
|
|
130
|
+
const megaContent = computed(() => {
|
|
131
|
+
const item = props.items.find((i) => i.id === openId.value && hasPanel(i));
|
|
132
|
+
if (!item || !megaFor(item)) return null;
|
|
133
|
+
const s = props.megaSettings;
|
|
134
|
+
const o = overrideFor(item);
|
|
135
|
+
const os = o?.settings ?? {};
|
|
136
|
+
const layout = up(os.LAYOUT) || up(s.HEADER_MENU_MEGA_LAYOUT, "LINKS_AND_FEATURED");
|
|
137
|
+
const style = up(os.FEATURED_STYLE) || up(s.HEADER_MENU_MEGA_FEATURED_STYLE, "GRID");
|
|
138
|
+
|
|
139
|
+
const columns = [];
|
|
140
|
+
let plain = null;
|
|
141
|
+
for (const child of item.children) {
|
|
142
|
+
if (child.children?.length) {
|
|
143
|
+
columns.push({ key: child.id, heading: child, links: child.children });
|
|
144
|
+
} else {
|
|
145
|
+
if (!plain) columns.push((plain = { key: `plain-${item.id}`, heading: null, links: [] }));
|
|
146
|
+
plain.links.push(child);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const max = Number(s.HEADER_MENU_MEGA_CARDS_MAX) || 8;
|
|
151
|
+
let cards = [];
|
|
152
|
+
const source = up(os.FEATURED, "MENU_IMAGES");
|
|
153
|
+
if (layout !== "LINKS_ONLY" && s.HEADER_MENU_MEGA_IMAGES !== false) {
|
|
154
|
+
if (o && source === "PRODUCTS") {
|
|
155
|
+
loadProducts(o);
|
|
156
|
+
const ids = (os.PRODUCTS ?? []).map((v) => (v && typeof v === "object" ? v.id : v)).filter(Boolean);
|
|
157
|
+
const rows = productCache[`${o.key}:${ids.join(",")}`]?.rows ?? [];
|
|
158
|
+
cards = rows.map((p) => ({
|
|
159
|
+
key: `p-${p.id}`, title: p.name, url: productUrl(p), image: mediaUrl(p.main_image),
|
|
160
|
+
price: os.SHOW_PRICE !== false ? priceOf(p) : "",
|
|
161
|
+
}));
|
|
162
|
+
} else if (o && source === "CUSTOM") {
|
|
163
|
+
cards = [1, 2, 3, 4]
|
|
164
|
+
.map((n) => ({
|
|
165
|
+
key: `c-${n}`, title: t(os[`TITLE_${n}`]) || "", url: os[`URL_${n}`] || null,
|
|
166
|
+
image: mediaUrl(os[`IMAGE_${n}`]?.path ?? os[`IMAGE_${n}`] ?? ""),
|
|
167
|
+
}))
|
|
168
|
+
.filter((c) => c.image);
|
|
169
|
+
} else {
|
|
170
|
+
cards = item.children
|
|
171
|
+
.filter((c) => c.image)
|
|
172
|
+
.map((c) => ({ key: `m-${c.id}`, title: c.title, url: c.url, image: mediaUrl(c.image) }));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
cards = cards.slice(0, max);
|
|
176
|
+
|
|
177
|
+
// A link with no sub-links has only its featured cards to show.
|
|
178
|
+
let shown = layout === "LINKS_AND_FEATURED" && !cards.length ? "LINKS_ONLY" : layout;
|
|
179
|
+
if (!columns.length) shown = "FEATURED_ONLY";
|
|
180
|
+
return {
|
|
181
|
+
key: item.id,
|
|
182
|
+
item,
|
|
183
|
+
layout: shown,
|
|
184
|
+
style,
|
|
185
|
+
columns: layout === "FEATURED_ONLY" ? [] : columns,
|
|
186
|
+
cards,
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
/** The mega menu's look as CSS variables (HEADER_MENU_MEGA_*). */
|
|
191
|
+
const megaVars = computed(() => {
|
|
192
|
+
const s = props.megaSettings;
|
|
193
|
+
const RATIOS = { SQUARE: "1 / 1", PORTRAIT: "3 / 4", LANDSCAPE: "4 / 3" };
|
|
194
|
+
const JUSTIFY = { BETWEEN: "space-between", START: "flex-start", CENTER: "center" };
|
|
195
|
+
return Object.fromEntries(Object.entries({
|
|
196
|
+
"--mega-bg": s.HEADER_MENU_MEGA_BACKGROUND || undefined,
|
|
197
|
+
"--mega-featured-w": `${Number(s.HEADER_MENU_MEGA_FEATURED_WIDTH) || 45}%`,
|
|
198
|
+
"--mega-per": String(Math.max(1, Number(s.HEADER_MENU_MEGA_CARDS) || 4)),
|
|
199
|
+
"--mega-ratio": RATIOS[up(s.HEADER_MENU_MEGA_IMAGE_RATIO, "PORTRAIT")],
|
|
200
|
+
"--mega-card-gap": px(s.HEADER_MENU_MEGA_CARD_GAP),
|
|
201
|
+
"--mega-card-r": px(s.HEADER_MENU_MEGA_CARD_RADIUS),
|
|
202
|
+
"--mega-overlay": String((Number(s.HEADER_MENU_MEGA_CARD_OVERLAY ?? 30) || 0) / 100),
|
|
203
|
+
"--mega-title-justify": JUSTIFY[up(s.HEADER_MENU_MEGA_CARD_ALIGN, "BETWEEN")],
|
|
204
|
+
"--mega-title-gap": px(s.HEADER_MENU_MEGA_CARD_TITLE_GAP),
|
|
205
|
+
"--mega-title-fs": px(s.HEADER_MENU_MEGA_CARD_TITLE_SIZE),
|
|
206
|
+
"--mega-title-fw": weightOf(s.HEADER_MENU_MEGA_CARD_TITLE_WEIGHT),
|
|
207
|
+
"--mega-title-tt": CASES[up(s.HEADER_MENU_MEGA_CARD_TITLE_CASE)],
|
|
208
|
+
"--mega-title-color": s.HEADER_MENU_MEGA_CARD_TITLE_COLOR || undefined,
|
|
209
|
+
"--mega-col-w": px(s.HEADER_MENU_MEGA_COLUMN_WIDTH),
|
|
210
|
+
"--mega-head-fs": px(s.HEADER_MENU_MEGA_HEADING_SIZE),
|
|
211
|
+
"--mega-head-fw": weightOf(s.HEADER_MENU_MEGA_HEADING_WEIGHT),
|
|
212
|
+
"--mega-head-tt": CASES[up(s.HEADER_MENU_MEGA_HEADING_CASE)],
|
|
213
|
+
"--mega-head-color": s.HEADER_MENU_MEGA_HEADING_COLOR || undefined,
|
|
214
|
+
"--mega-link-fs": px(s.HEADER_MENU_MEGA_LINK_SIZE),
|
|
215
|
+
"--mega-link-gap": px(s.HEADER_MENU_MEGA_LINK_GAP),
|
|
216
|
+
"--mega-link-color": s.HEADER_MENU_MEGA_LINK_COLOR || undefined,
|
|
217
|
+
"--mega-link-hover": s.HEADER_MENU_MEGA_LINK_HOVER_COLOR || undefined,
|
|
218
|
+
}).filter(([, v]) => v !== undefined));
|
|
219
|
+
});
|
|
220
|
+
const cardTitle = computed(() => up(props.megaSettings.HEADER_MENU_MEGA_CARD_TITLE, "BELOW"));
|
|
221
|
+
const titleAt = computed(() => up(props.megaSettings.HEADER_MENU_MEGA_CARD_TITLE_POSITION, "BOTTOM").toLowerCase());
|
|
222
|
+
const showArrow = computed(() => !!props.megaSettings.HEADER_MENU_MEGA_CARD_ARROW);
|
|
223
|
+
const arrowSvg = computed(() => String(props.megaSettings.HEADER_MENU_MEGA_CARD_ARROW_SVG || "").trim());
|
|
224
|
+
|
|
225
|
+
/* Carousel arrows: scroll the track by one card. */
|
|
226
|
+
const tracks = new Map();
|
|
227
|
+
function trackRef(key) {
|
|
228
|
+
return (el) => (el ? tracks.set(key, el) : tracks.delete(key));
|
|
229
|
+
}
|
|
230
|
+
function scrollCards(key, dir) {
|
|
231
|
+
const el = tracks.get(key);
|
|
232
|
+
if (!el) return;
|
|
233
|
+
const card = el.querySelector(".osi-mega-card");
|
|
234
|
+
const step = (card?.offsetWidth || el.clientWidth / 2) + 16;
|
|
235
|
+
const rtl = getComputedStyle(el).direction === "rtl" ? -1 : 1;
|
|
236
|
+
el.scrollBy({ left: dir * step * rtl, behavior: "smooth" });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The panel's height follows its content: measured (and re-measured as images
|
|
241
|
+
* load) on whichever content is current, and animated by CSS. From closed it
|
|
242
|
+
* grows from 0; from one menu to the next it grows or shrinks.
|
|
243
|
+
*/
|
|
244
|
+
const panelHeight = ref(0);
|
|
245
|
+
let observer = null;
|
|
246
|
+
function trackContent(el) {
|
|
247
|
+
// Vue calls this with null for the content leaving — keep observing the new one.
|
|
248
|
+
if (!el || !import.meta.client) return;
|
|
249
|
+
observer?.disconnect();
|
|
250
|
+
const measure = () => (panelHeight.value = el.offsetHeight);
|
|
251
|
+
measure();
|
|
252
|
+
observer = new ResizeObserver(measure);
|
|
253
|
+
observer.observe(el);
|
|
254
|
+
}
|
|
255
|
+
watch(megaContent, (content) => {
|
|
256
|
+
if (!content) {
|
|
257
|
+
observer?.disconnect();
|
|
258
|
+
panelHeight.value = 0;
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
onScopeDispose(() => {
|
|
263
|
+
clearTimers();
|
|
264
|
+
observer?.disconnect();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
return { isActive, openId, openTimer, closeTimer, clearTimers, open, close, toggle, onCaret, LinkComponent, mega, mediaUrl, t, productUrl, store, up, px, WEIGHTS, weightOf, CASES, overrideFor, megaFor, anyMega, hasPanel, productCache, loadProducts, currency, priceOf, megaContent, megaVars, cardTitle, titleAt, showArrow, arrowSvg, tracks, trackRef, scrollCards, panelHeight, observer, trackContent };
|
|
268
|
+
}
|