@osimart/create-storefront 0.0.0-stage → 1.0.1
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.js +24 -0
- package/app/plugins/reveal.client.js +53 -0
- package/app/plugins/toast.js +10 -0
- package/app/plugins/vue-query.js +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 +282 -0
- package/nuxt/osi-sections.ts +56 -0
- package/nuxt.config.ts +60 -0
- package/package.json +44 -4
- 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,211 @@
|
|
|
1
|
+
import { useProductResource, createProductDetail, provideProductDetail } from "#osi/composables/useProductDetail";
|
|
2
|
+
|
|
3
|
+
export default function useLogic({ props }) {
|
|
4
|
+
const { s, shellStyle, innerStyle, customClass } = useSectionShell(() => props.section);
|
|
5
|
+
|
|
6
|
+
const has = (v) => v !== undefined && v !== null && v !== "";
|
|
7
|
+
const px = (v) => (has(v) && Number.isFinite(Number(v)) ? `${Number(v)}px` : undefined);
|
|
8
|
+
const compact = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => has(v)));
|
|
9
|
+
const WEIGHTS = { LIGHT: 300, REGULAR: 400, MEDIUM: 500, SEMIBOLD: 600, BOLD: 700 };
|
|
10
|
+
const CASES = { NONE: "none", UPPERCASE: "uppercase", LOWERCASE: "lowercase", CAPITALIZE: "capitalize" };
|
|
11
|
+
|
|
12
|
+
// FEATURED_PRODUCT passes the picked product; the product page uses the route.
|
|
13
|
+
const { product, loading } = useProductResource(
|
|
14
|
+
props.productId !== undefined ? { productId: () => props.productId } : {}
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const detail = provideProductDetail(
|
|
18
|
+
createProductDetail({
|
|
19
|
+
product,
|
|
20
|
+
settings: () => s.value,
|
|
21
|
+
loading,
|
|
22
|
+
})
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The default column, for a theme whose MAIN_PRODUCT template ships no blocks.
|
|
27
|
+
* Deliberately close to what the reference store had, minus the store-specific
|
|
28
|
+
* accordion copy — policies come from the store, so those two rows are safe to
|
|
29
|
+
* default in.
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_BLOCKS = [
|
|
32
|
+
{ id: "default-title", type: "TITLE", settings: {} },
|
|
33
|
+
{ id: "default-price", type: "PRICE", settings: {} },
|
|
34
|
+
{ id: "default-picker", type: "VARIANT_PICKER", settings: { PICKER_STYLE: "SWATCHES" } },
|
|
35
|
+
{
|
|
36
|
+
id: "default-buy",
|
|
37
|
+
type: "BUY_BUTTONS",
|
|
38
|
+
settings: { BUY_SHOW_QUANTITY: true, BUY_SHOW_STOCK_NOTICE: true },
|
|
39
|
+
},
|
|
40
|
+
{ id: "default-description", type: "DESCRIPTION", settings: {} },
|
|
41
|
+
{
|
|
42
|
+
id: "default-shipping",
|
|
43
|
+
type: "COLLAPSIBLE",
|
|
44
|
+
settings: {
|
|
45
|
+
COLLAPSIBLE_HEADING: "Processing & delivery",
|
|
46
|
+
COLLAPSIBLE_SOURCE: "SHIPPING_POLICY",
|
|
47
|
+
ICON_PRESET: "truck",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "default-returns",
|
|
52
|
+
type: "COLLAPSIBLE",
|
|
53
|
+
settings: {
|
|
54
|
+
COLLAPSIBLE_HEADING: "Exchanges & returns",
|
|
55
|
+
COLLAPSIBLE_SOURCE: "RETURN_POLICY",
|
|
56
|
+
ICON_PRESET: "return",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{ id: "default-share", type: "SHARE", settings: {} },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const blocks = computed(() => {
|
|
63
|
+
const authored = (props.section.blocks ?? []).filter((block) => !block.disabled);
|
|
64
|
+
return authored.length ? authored : DEFAULT_BLOCKS;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/* ------------------------------------------------------------------ layout */
|
|
68
|
+
|
|
69
|
+
const mediaWidth = computed(() => {
|
|
70
|
+
const value = Number(s.value.MEDIA_WIDTH);
|
|
71
|
+
return Number.isFinite(value) ? Math.min(Math.max(value, 30), 70) : 60;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const ALIGN = { START: "flex-start", CENTER: "center", END: "flex-end" };
|
|
75
|
+
const TEXT_ALIGN = { START: "start", CENTER: "center", END: "end" };
|
|
76
|
+
|
|
77
|
+
const layoutStyle = computed(() => {
|
|
78
|
+
const v = s.value;
|
|
79
|
+
const align = String(v.INFO_ALIGN || "START").toUpperCase();
|
|
80
|
+
return compact({
|
|
81
|
+
// flex-grow weights rather than percentages: with a gap in the row,
|
|
82
|
+
// 60% + 40% + gap overflows.
|
|
83
|
+
"--osi-pdp-media-grow": String(mediaWidth.value),
|
|
84
|
+
"--osi-pdp-info-grow": String(100 - mediaWidth.value),
|
|
85
|
+
"--osi-pdp-column-gap": px(v.COLUMN_GAP) ?? "48px",
|
|
86
|
+
"--osi-pdp-column-gap-mobile": px(v.COLUMN_GAP_MOBILE),
|
|
87
|
+
"--osi-pdp-info-max": v.INFO_MAX_WIDTH ? `${Number(v.INFO_MAX_WIDTH)}px` : "none",
|
|
88
|
+
// Stacked under the gallery (empty / 0: full width).
|
|
89
|
+
"--osi-pdp-info-max-m": v.INFO_MAX_WIDTH_MOBILE ? `${Number(v.INFO_MAX_WIDTH_MOBILE)}px` : "none",
|
|
90
|
+
// Where the capped box sits inside its column (margins, not alignment:
|
|
91
|
+
// the column is a plain block, so `auto` margins are what move it).
|
|
92
|
+
"--osi-pdp-info-ml": { CENTER: "auto", END: "auto" }[String(v.INFO_BOX_POSITION || "START").toUpperCase()] ?? "0",
|
|
93
|
+
"--osi-pdp-info-mr": { CENTER: "auto", START: "auto" }[String(v.INFO_BOX_POSITION || "START").toUpperCase()] ?? "0",
|
|
94
|
+
"--osi-pdp-info-vertical":
|
|
95
|
+
{ CENTER: "center", BOTTOM: "flex-end" }[String(v.INFO_VERTICAL_ALIGN || "TOP").toUpperCase()] ?? "flex-start",
|
|
96
|
+
"--osi-pdp-sticky-top": px(v.INFO_STICKY_OFFSET) ?? "96px",
|
|
97
|
+
// Read by every block (and by the rows inside them) so "centre the
|
|
98
|
+
// details" moves the price too, not just the headings.
|
|
99
|
+
"--osi-pdp-justify": ALIGN[align] ?? "flex-start",
|
|
100
|
+
"--osi-pdp-text-align": TEXT_ALIGN[align] ?? "start",
|
|
101
|
+
|
|
102
|
+
// the gallery's own padding and cap
|
|
103
|
+
"--osi-pdp-media-pt": px(v.MEDIA_PADDING_TOP),
|
|
104
|
+
"--osi-pdp-media-pb": px(v.MEDIA_PADDING_BOTTOM),
|
|
105
|
+
"--osi-pdp-media-pl": px(v.MEDIA_PADDING_LEFT),
|
|
106
|
+
"--osi-pdp-media-pr": px(v.MEDIA_PADDING_RIGHT),
|
|
107
|
+
"--osi-pdp-media-max": px(v.MEDIA_MAX_WIDTH),
|
|
108
|
+
|
|
109
|
+
// details column as a card
|
|
110
|
+
"--osi-pdp-info-bg": v.INFO_BG_COLOR,
|
|
111
|
+
"--osi-pdp-info-pt": px(v.INFO_PADDING_TOP),
|
|
112
|
+
"--osi-pdp-info-pb": px(v.INFO_PADDING_BOTTOM),
|
|
113
|
+
"--osi-pdp-info-pl": px(v.INFO_PADDING_LEFT),
|
|
114
|
+
"--osi-pdp-info-pr": px(v.INFO_PADDING_RIGHT),
|
|
115
|
+
"--osi-pdp-info-radius": px(v.INFO_RADIUS),
|
|
116
|
+
"--osi-pdp-info-border":
|
|
117
|
+
has(v.INFO_BORDER_WIDTH) && Number(v.INFO_BORDER_WIDTH) > 0
|
|
118
|
+
? `${Number(v.INFO_BORDER_WIDTH)}px solid ${v.INFO_BORDER_COLOR || "var(--osi-border, #e5e5e5)"}`
|
|
119
|
+
: undefined,
|
|
120
|
+
|
|
121
|
+
// accordions
|
|
122
|
+
"--acc-line":
|
|
123
|
+
String(v.ACCORDION_DIVIDERS || "LINES").toUpperCase() === "NONE"
|
|
124
|
+
? "0 solid transparent"
|
|
125
|
+
: `${Number(v.ACCORDION_LINE_WIDTH ?? 1)}px solid ${v.ACCORDION_LINE_COLOR || "var(--osi-border, #e5e5e5)"}`,
|
|
126
|
+
"--acc-radius": px(v.ACCORDION_RADIUS),
|
|
127
|
+
"--acc-gap": px(v.ACCORDION_GAP),
|
|
128
|
+
"--acc-head-py": px(v.ACCORDION_HEAD_PADDING_Y),
|
|
129
|
+
"--acc-head-px": px(v.ACCORDION_HEAD_PADDING_X),
|
|
130
|
+
"--acc-body-pt": px(v.ACCORDION_BODY_PADDING_TOP),
|
|
131
|
+
"--acc-body-pb": px(v.ACCORDION_BODY_PADDING_BOTTOM),
|
|
132
|
+
"--acc-head-bg": v.ACCORDION_HEAD_BG,
|
|
133
|
+
"--acc-body-bg": v.ACCORDION_BODY_BG,
|
|
134
|
+
"--acc-bg": v.ACCORDION_HEAD_BG,
|
|
135
|
+
"--acc-title-fs": px(v.ACCORDION_TITLE_SIZE),
|
|
136
|
+
"--acc-title-fw": WEIGHTS[v.ACCORDION_TITLE_WEIGHT],
|
|
137
|
+
"--acc-title-ls": has(v.ACCORDION_TITLE_LETTER_SPACING)
|
|
138
|
+
? `${Number(v.ACCORDION_TITLE_LETTER_SPACING) / 100}em`
|
|
139
|
+
: undefined,
|
|
140
|
+
"--acc-title-tt": CASES[v.ACCORDION_TITLE_TRANSFORM],
|
|
141
|
+
"--acc-title-color": v.ACCORDION_TITLE_COLOR,
|
|
142
|
+
"--acc-title-ff": v.ACCORDION_TITLE_FONT === "HEADING" ? "var(--osi-font-heading)" : undefined,
|
|
143
|
+
"--acc-icon-size": px(v.ACCORDION_ICON_SIZE),
|
|
144
|
+
"--acc-body-fs": px(v.ACCORDION_BODY_SIZE),
|
|
145
|
+
"--acc-body-color": v.ACCORDION_BODY_COLOR,
|
|
146
|
+
"--acc-body-lh": has(v.ACCORDION_BODY_LINE_HEIGHT)
|
|
147
|
+
? String(Number(v.ACCORDION_BODY_LINE_HEIGHT) / 100)
|
|
148
|
+
: undefined,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const layoutClasses = computed(() => [
|
|
153
|
+
s.value.MEDIA_POSITION === "END" ? "osi-pdp--media-end" : "osi-pdp--media-start",
|
|
154
|
+
{
|
|
155
|
+
"osi-pdp--sticky": s.value.INFO_STICKY !== false,
|
|
156
|
+
"osi-pdp--featured": props.featured,
|
|
157
|
+
"osi-pdp--info-first": !!s.value.MOBILE_INFO_FIRST,
|
|
158
|
+
"osi-pdp--info-card": !!(s.value.INFO_BG_COLOR || Number(s.value.INFO_BORDER_WIDTH) > 0),
|
|
159
|
+
},
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
/** Accordion look: classes, because CSS can't branch on a variable. */
|
|
163
|
+
const accordionClasses = computed(() => {
|
|
164
|
+
const v = s.value;
|
|
165
|
+
const dividers = String(v.ACCORDION_DIVIDERS || "LINES").toUpperCase();
|
|
166
|
+
return [
|
|
167
|
+
`osi-acc-icon--${String(v.ACCORDION_ICON || "PLUS").toLowerCase()}`,
|
|
168
|
+
`osi-acc-icon-pos--${String(v.ACCORDION_ICON_POSITION || "END").toLowerCase()}`,
|
|
169
|
+
dividers === "BOXED" ? "osi-acc--boxed" : dividers === "NONE" ? "osi-acc--none" : "osi-acc--lines",
|
|
170
|
+
];
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ACCORDION_ONE_OPEN: the blocks share one open row through this.
|
|
174
|
+
provide("osi-pdp-accordion", {
|
|
175
|
+
single: computed(() => !!s.value.ACCORDION_ONE_OPEN),
|
|
176
|
+
openId: ref(null),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* PAGE_BACKGROUND paints the page behind the sections, not just this one.
|
|
181
|
+
* A stylesheet rather than an inline style: .osi-root is the page root and
|
|
182
|
+
* this section can't reach it, and the optional "all sections" mode needs a
|
|
183
|
+
* descendant selector.
|
|
184
|
+
*/
|
|
185
|
+
const pageBackgroundCss = computed(() => {
|
|
186
|
+
const color = s.value.PAGE_BACKGROUND;
|
|
187
|
+
if (!color) return "";
|
|
188
|
+
const all = s.value.PAGE_BACKGROUND_ALL
|
|
189
|
+
? `.osi-root .osi-section{background:${color} !important}`
|
|
190
|
+
: "";
|
|
191
|
+
return `.osi-root{background:${color}}${all}`;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
useHead(() => ({
|
|
195
|
+
style: pageBackgroundCss.value
|
|
196
|
+
? [{ key: `osi-pdp-page-bg-${props.section.id}`, innerHTML: pageBackgroundCss.value }]
|
|
197
|
+
: [],
|
|
198
|
+
}));
|
|
199
|
+
|
|
200
|
+
// Per instance, so two product sections on one page can't overwrite each
|
|
201
|
+
// other's stylesheet.
|
|
202
|
+
useScopedCss(
|
|
203
|
+
() => s.value.CUSTOM_CSS,
|
|
204
|
+
computed(() => `[data-osi-section="${props.section.id}"]`),
|
|
205
|
+
`osi-section-css-${props.section.id}`
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const showSkeleton = computed(() => loading.value && !detail.product.value);
|
|
209
|
+
|
|
210
|
+
return { s, shellStyle, innerStyle, customClass, has, px, compact, WEIGHTS, CASES, product, loading, detail, DEFAULT_BLOCKS, blocks, mediaWidth, ALIGN, TEXT_ALIGN, layoutStyle, layoutClasses, accordionClasses, pageBackgroundCss, showSkeleton };
|
|
211
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAP — an embedded map.
|
|
3
|
+
*
|
|
4
|
+
* MAP_SOURCE
|
|
5
|
+
* COORDINATES LATITUDE / LONGITUDE / ZOOM
|
|
6
|
+
* EMBED EMBED_URL: whatever Google Maps gives — the "Embed a map"
|
|
7
|
+
* HTML (the <iframe> code), the link inside it, or a place
|
|
8
|
+
* link with coordinates (/maps/place/…/@lat,lng,15z). Other
|
|
9
|
+
* providers' embed links (…/embed…) pass through as they are.
|
|
10
|
+
* An older section with EMBED_URL and no MAP_SOURCE counts as EMBED.
|
|
11
|
+
*
|
|
12
|
+
* MAP_HEIGHT / MAP_HEIGHT_MOBILE (HEIGHT / HEIGHT_MOBILE: older names),
|
|
13
|
+
* MAP_LINK_LABEL (empty hides the link).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export default function useLogic({ props }) {
|
|
18
|
+
const { s, t, shellStyle, innerStyle, align, customClass, heading } =
|
|
19
|
+
useSectionShell(() => props.section);
|
|
20
|
+
const { isPreview } = useBuilderPreview();
|
|
21
|
+
|
|
22
|
+
const num = (v, d) => (Number.isFinite(Number(v)) && v !== "" && v != null ? Number(v) : d);
|
|
23
|
+
const height = computed(() => num(s.value.MAP_HEIGHT ?? s.value.HEIGHT, 420));
|
|
24
|
+
const heightMobile = computed(() => num(s.value.MAP_HEIGHT_MOBILE ?? s.value.HEIGHT_MOBILE, 280));
|
|
25
|
+
const label = computed(() => t(s.value.ADDRESS_LABEL) || t(s.value.HEADING) || "Map");
|
|
26
|
+
const linkLabel = computed(() => {
|
|
27
|
+
const v = s.value.MAP_LINK_LABEL;
|
|
28
|
+
return v === undefined ? "Open in Google Maps" : t(v);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const source = computed(() => {
|
|
32
|
+
const v = String(s.value.MAP_SOURCE || "").toUpperCase();
|
|
33
|
+
if (v) return v;
|
|
34
|
+
return s.value.EMBED_URL ? "EMBED" : "COORDINATES";
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/** Google Maps URLs for a query (coordinates or text). */
|
|
38
|
+
const googleEmbed = (q, zoom) =>
|
|
39
|
+
`https://www.google.com/maps?q=${encodeURIComponent(q)}${zoom ? `&z=${zoom}` : ""}&output=embed`;
|
|
40
|
+
const googleLink = (q) => `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(q)}`;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* What was pasted -> { src, href } (href: the "open in Maps" link, when one
|
|
44
|
+
* can be made), or null when it can't be embedded.
|
|
45
|
+
*/
|
|
46
|
+
function fromEmbed(raw) {
|
|
47
|
+
let value = String(raw || "").trim();
|
|
48
|
+
if (!value) return null;
|
|
49
|
+
// The whole <iframe …> code: take its src.
|
|
50
|
+
const tag = value.match(/src\s*=\s*["']([^"']+)["']/i);
|
|
51
|
+
if (tag) value = tag[1];
|
|
52
|
+
value = value.replace(/&/g, "&");
|
|
53
|
+
|
|
54
|
+
let url;
|
|
55
|
+
try {
|
|
56
|
+
url = new URL(value);
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (url.protocol !== "https:") return null;
|
|
61
|
+
|
|
62
|
+
// Already an embed (Google's /maps/embed?pb=…, ?output=embed, others' /embed).
|
|
63
|
+
if (url.pathname.includes("/embed") || url.searchParams.get("output") === "embed") {
|
|
64
|
+
return { src: url.toString(), href: null };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const google = /(^|\.)google\.[a-z.]+$/i.test(url.hostname);
|
|
68
|
+
if (!google) return null;
|
|
69
|
+
|
|
70
|
+
// A place / map link with coordinates: …/@33.83,35.54,15z
|
|
71
|
+
const at = url.pathname.match(/@(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)(?:,(\d+(?:\.\d+)?)z)?/);
|
|
72
|
+
if (at) {
|
|
73
|
+
const q = `${at[1]},${at[2]}`;
|
|
74
|
+
return { src: googleEmbed(q, at[3] ? Math.round(Number(at[3])) : 15), href: googleLink(q) };
|
|
75
|
+
}
|
|
76
|
+
// ?q=… or /maps/search/<query> or /maps/place/<name>
|
|
77
|
+
const q =
|
|
78
|
+
url.searchParams.get("q") ||
|
|
79
|
+
url.searchParams.get("query") ||
|
|
80
|
+
decodeURIComponent((url.pathname.match(/\/maps\/(?:search|place)\/([^/]+)/) || [])[1] || "").replace(/\+/g, " ");
|
|
81
|
+
if (q) return { src: googleEmbed(q), href: googleLink(q) };
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const map = computed(() => {
|
|
86
|
+
if (source.value === "EMBED") return fromEmbed(s.value.EMBED_URL);
|
|
87
|
+
const q = `${num(s.value.LATITUDE, 33.8339)},${num(s.value.LONGITUDE, 35.5442)}`;
|
|
88
|
+
return { src: googleEmbed(q, num(s.value.ZOOM, 15)), href: googleLink(q) };
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return { s, t, shellStyle, innerStyle, align, customClass, heading, isPreview, num, height, heightMobile, label, linkLabel, source, googleEmbed, googleLink, fromEmbed, map };
|
|
92
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
|
|
2
|
+
export default function useLogic({ props }) {
|
|
3
|
+
const { s, shellStyle, innerStyle, customClass } = useSectionShell(() => props.section);
|
|
4
|
+
|
|
5
|
+
/** The section has a height of its own (anything but Auto). */
|
|
6
|
+
const sized = computed(() => String(s.value.HEIGHT || "AUTO").toUpperCase() !== "AUTO");
|
|
7
|
+
|
|
8
|
+
const { tree, backgroundOf, editing, u } = useContentTree(() => props.section, {
|
|
9
|
+
rootDefaults: props.rootDefaults,
|
|
10
|
+
fillRootImages: sized,
|
|
11
|
+
index: () => props.index,
|
|
12
|
+
custom: props.custom,
|
|
13
|
+
customText: props.customText,
|
|
14
|
+
});
|
|
15
|
+
const { num, px, compact, upper } = u;
|
|
16
|
+
|
|
17
|
+
/* --------------------------------------------------------- section shell */
|
|
18
|
+
|
|
19
|
+
const HEIGHTS = { SMALL: "40vh", MEDIUM: "60vh", LARGE: "80vh", FULL: "100svh" };
|
|
20
|
+
const background = computed(() => backgroundOf(s.value));
|
|
21
|
+
|
|
22
|
+
const sectionStyle = computed(() => {
|
|
23
|
+
const v = s.value;
|
|
24
|
+
const height = upper(v.HEIGHT, "AUTO");
|
|
25
|
+
return {
|
|
26
|
+
...shellStyle.value,
|
|
27
|
+
...compact({
|
|
28
|
+
minHeight: height === "CUSTOM" ? px(v.HEIGHT_CUSTOM) : HEIGHTS[height],
|
|
29
|
+
border: upper(v.BORDER_STYLE, "NONE") === "SOLID"
|
|
30
|
+
? `${num(v.BORDER_WIDTH, 1)}px solid ${v.BORDER_COLOR || "currentColor"}`
|
|
31
|
+
: undefined,
|
|
32
|
+
borderRadius: num(v.BORDER_RADIUS, 0) ? `${num(v.BORDER_RADIUS, 0)}px` : undefined,
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return { s, shellStyle, innerStyle, customClass, sized, tree, backgroundOf, editing, u, num, px, compact, upper, HEIGHTS, background, sectionStyle };
|
|
38
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { NuxtLink } from "#components";
|
|
2
|
+
import { buttonClasses as toButtonClasses, buttonVars } from "#osi/composables/useButtonStyle";
|
|
3
|
+
|
|
4
|
+
export default function useLogic({ props }) {
|
|
5
|
+
const isMwt = computed(() => String(props.section.type || "").toUpperCase() === "MEDIA_WITH_TEXT");
|
|
6
|
+
|
|
7
|
+
const { s, t, shellStyle, innerStyle, customClass, heading } = useSectionShell(() => props.section);
|
|
8
|
+
const { mediaUrl, posterUrl } = useMediaUrl();
|
|
9
|
+
const { isVideo } = useMediaTypes();
|
|
10
|
+
const { isMobile } = useLayoutShell();
|
|
11
|
+
|
|
12
|
+
const has = (v) => v !== undefined && v !== null && v !== "";
|
|
13
|
+
const px = (v) => (has(v) && Number.isFinite(Number(v)) ? `${Number(v)}px` : undefined);
|
|
14
|
+
const compact = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => has(v)));
|
|
15
|
+
const WEIGHTS = { LIGHT: 300, REGULAR: 400, MEDIUM: 500, SEMIBOLD: 600, BOLD: 700 };
|
|
16
|
+
const CASES = { NONE: "none", UPPERCASE: "uppercase", LOWERCASE: "lowercase", CAPITALIZE: "capitalize" };
|
|
17
|
+
|
|
18
|
+
/* --------------------------------------------------------------- media */
|
|
19
|
+
|
|
20
|
+
const media = computed(() =>
|
|
21
|
+
isMobile.value ? s.value.MOBILE_MEDIA || s.value.MEDIA_MOBILE || s.value.MEDIA : s.value.MEDIA
|
|
22
|
+
);
|
|
23
|
+
const mediaSrc = computed(() => mediaUrl(media.value ?? null));
|
|
24
|
+
const mediaPath = computed(() => media.value?.path ?? media.value ?? "");
|
|
25
|
+
const isVideoMedia = computed(() => !!mediaPath.value && isVideo(mediaPath.value));
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Video: an uploaded file (MEDIA) or a YouTube / Vimeo link (VIDEO_URL).
|
|
29
|
+
* Playback defaults to a silent loop, which is the only way browsers allow
|
|
30
|
+
* autoplay; turning sound on means turning autoplay off.
|
|
31
|
+
*/
|
|
32
|
+
const videoAutoplay = computed(() => s.value.VIDEO_AUTOPLAY !== false);
|
|
33
|
+
const videoLoop = computed(() => s.value.VIDEO_LOOP !== false);
|
|
34
|
+
const videoMuted = computed(() => videoAutoplay.value || s.value.VIDEO_MUTED !== false);
|
|
35
|
+
const videoControls = computed(() => !!s.value.VIDEO_CONTROLS);
|
|
36
|
+
const videoPoster = computed(() => mediaUrl(s.value.VIDEO_POSTER ?? null) || posterUrl(media.value));
|
|
37
|
+
|
|
38
|
+
const embedUrl = computed(() => {
|
|
39
|
+
const raw = String(s.value.VIDEO_URL || "").trim();
|
|
40
|
+
if (!raw) return null;
|
|
41
|
+
const flags = (extra) =>
|
|
42
|
+
new URLSearchParams({
|
|
43
|
+
autoplay: videoAutoplay.value ? "1" : "0",
|
|
44
|
+
mute: videoMuted.value ? "1" : "0",
|
|
45
|
+
muted: videoMuted.value ? "1" : "0",
|
|
46
|
+
loop: videoLoop.value ? "1" : "0",
|
|
47
|
+
controls: videoControls.value ? "1" : "0",
|
|
48
|
+
playsinline: "1",
|
|
49
|
+
...extra,
|
|
50
|
+
}).toString();
|
|
51
|
+
const yt = raw.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{6,})/);
|
|
52
|
+
if (yt) return `https://www.youtube-nocookie.com/embed/${yt[1]}?${flags({ playlist: yt[1], rel: "0" })}`;
|
|
53
|
+
const vm = raw.match(/vimeo\.com\/(?:video\/)?(\d+)/);
|
|
54
|
+
if (vm) return `https://player.vimeo.com/video/${vm[1]}?${flags({ background: videoControls.value ? "0" : "1" })}`;
|
|
55
|
+
return null;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// No picture, video or embed yet: the section's own background (colour scheme /
|
|
59
|
+
// BACKGROUND_COLOR) has to show instead of the media's grey placeholder.
|
|
60
|
+
const hasMedia = computed(() => !!embedUrl.value || !!mediaSrc.value);
|
|
61
|
+
|
|
62
|
+
const RATIOS = {
|
|
63
|
+
NATURAL: "auto",
|
|
64
|
+
PORTRAIT: "3 / 4",
|
|
65
|
+
SQUARE: "1 / 1",
|
|
66
|
+
LANDSCAPE: "4 / 3",
|
|
67
|
+
WIDE: "16 / 9",
|
|
68
|
+
ULTRAWIDE: "21 / 9",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/* ---------------------------------------------------------------- text */
|
|
72
|
+
|
|
73
|
+
const eyebrow = computed(() => t(s.value.EYEBROW));
|
|
74
|
+
const body = computed(() => t(s.value.BODY));
|
|
75
|
+
// HERO's plain-text line under the heading.
|
|
76
|
+
const subheading = computed(() => t(s.value.SUBHEADING));
|
|
77
|
+
// HERO's TEXT blocks: extra lines, each with its own size.
|
|
78
|
+
const textBlocks = computed(() =>
|
|
79
|
+
(props.section.blocks ?? [])
|
|
80
|
+
.filter((b) => !b.disabled && b.type === "TEXT" && t(b.settings?.TEXT))
|
|
81
|
+
.map((b) => ({
|
|
82
|
+
key: b.id ?? b.key,
|
|
83
|
+
text: t(b.settings.TEXT),
|
|
84
|
+
style: has(b.settings?.SIZE) ? { fontSize: `${Number(b.settings.SIZE)}px` } : undefined,
|
|
85
|
+
}))
|
|
86
|
+
);
|
|
87
|
+
const headingTag = computed(() => (s.value.HEADING_TAG === "H1" ? "h1" : "h2"));
|
|
88
|
+
// The first section on the page holds the LCP image: never lazy-load it.
|
|
89
|
+
const eager = computed(() => props.index === 0);
|
|
90
|
+
|
|
91
|
+
/** BUTTON blocks, else the single BUTTON_LABEL / BUTTON_URL of older sections. */
|
|
92
|
+
const buttons = computed(() => {
|
|
93
|
+
const blocks = (props.section.blocks ?? []).filter((b) => !b.disabled && b.type === "BUTTON");
|
|
94
|
+
const list = blocks
|
|
95
|
+
.map((b) => {
|
|
96
|
+
const bs = b.settings ?? {};
|
|
97
|
+
return {
|
|
98
|
+
key: b.id ?? b.key,
|
|
99
|
+
label: t(bs.LABEL),
|
|
100
|
+
url: bs.URL,
|
|
101
|
+
newTab: !!bs.OPEN_IN_NEW_TAB,
|
|
102
|
+
style: buttonVars(bs, "BUTTON"),
|
|
103
|
+
// HERO's older VARIANT (PRIMARY / SECONDARY / LINK) still works.
|
|
104
|
+
classes: toButtonClasses(bs, "BUTTON", [
|
|
105
|
+
"osi-mwt-button",
|
|
106
|
+
{ SECONDARY: "osi-btn--secondary", LINK: "osi-btn--underline" }[bs.VARIANT] ?? "",
|
|
107
|
+
]),
|
|
108
|
+
};
|
|
109
|
+
})
|
|
110
|
+
.filter((b) => b.label && b.url);
|
|
111
|
+
if (list.length) return list;
|
|
112
|
+
const label = t(s.value.BUTTON_LABEL);
|
|
113
|
+
const url = s.value.BUTTON_URL;
|
|
114
|
+
return label && url
|
|
115
|
+
? [{
|
|
116
|
+
key: "legacy",
|
|
117
|
+
label,
|
|
118
|
+
url,
|
|
119
|
+
newTab: false,
|
|
120
|
+
style: buttonVars(s.value, "BUTTON"),
|
|
121
|
+
classes: toButtonClasses(s.value, "BUTTON", ["osi-mwt-button"]),
|
|
122
|
+
}]
|
|
123
|
+
: [];
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const hasText = computed(
|
|
127
|
+
() =>
|
|
128
|
+
!!(eyebrow.value || heading.value || subheading.value || body.value ||
|
|
129
|
+
textBlocks.value.length || buttons.value.length)
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
/* -------------------------------------------------------------- layout */
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The merchant's choice, as is. SPLIT stays split even while the text is still
|
|
136
|
+
* empty — a freshly added section looks like the classic image-with-text, and
|
|
137
|
+
* the media only fills the row when "Media only" is picked.
|
|
138
|
+
*/
|
|
139
|
+
const layout = computed(() => {
|
|
140
|
+
const chosen = String(s.value.LAYOUT || "SPLIT").toUpperCase();
|
|
141
|
+
return ["SPLIT", "OVERLAY", "MEDIA_ONLY"].includes(chosen) ? chosen : "SPLIT";
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const fullBleed = computed(() => !!s.value.FULL_BLEED && layout.value !== "SPLIT");
|
|
145
|
+
|
|
146
|
+
// Whole-media link only when there are no buttons: a link inside a link is
|
|
147
|
+
// invalid HTML and the inner one stops being reachable by keyboard.
|
|
148
|
+
const mediaLink = computed(() => (s.value.LINK_URL && !buttons.value.length ? s.value.LINK_URL : null));
|
|
149
|
+
const NuxtLinkComponent = NuxtLink;
|
|
150
|
+
|
|
151
|
+
const ALIGN = { TOP: "start", CENTER: "center", BOTTOM: "end" };
|
|
152
|
+
const OVERLAY_SPOTS = {
|
|
153
|
+
TOP_LEFT: ["start", "start"], TOP_CENTER: ["start", "center"], TOP_RIGHT: ["start", "end"],
|
|
154
|
+
CENTER_LEFT: ["center", "start"], CENTER: ["center", "center"], CENTER_RIGHT: ["center", "end"],
|
|
155
|
+
BOTTOM_LEFT: ["end", "start"], BOTTOM_CENTER: ["end", "center"], BOTTOM_RIGHT: ["end", "end"],
|
|
156
|
+
};
|
|
157
|
+
const TEXT_ALIGN = { LEFT: "start", CENTER: "center", RIGHT: "end" };
|
|
158
|
+
const ITEMS_ALIGN = { LEFT: "flex-start", CENTER: "center", RIGHT: "flex-end" };
|
|
159
|
+
|
|
160
|
+
const vars = computed(() => {
|
|
161
|
+
const v = s.value;
|
|
162
|
+
const spot = OVERLAY_SPOTS[String(v.OVERLAY_POSITION || "CENTER").toUpperCase()] ?? OVERLAY_SPOTS.CENTER;
|
|
163
|
+
const textAlign = String(v.TEXT_ALIGNMENT || (layout.value === "OVERLAY" ? "CENTER" : "LEFT")).toUpperCase();
|
|
164
|
+
return compact({
|
|
165
|
+
// media
|
|
166
|
+
"--mwt-ratio": RATIOS[v.MEDIA_RATIO] ?? RATIOS.LANDSCAPE,
|
|
167
|
+
"--mwt-ratio-mobile": RATIOS[v.MEDIA_RATIO_MOBILE],
|
|
168
|
+
// A fixed px height, else HERO's SMALL / MEDIUM / LARGE / FULL screen share.
|
|
169
|
+
"--mwt-h": px(v.MEDIA_HEIGHT) ?? { SMALL: "40vh", MEDIUM: "60vh", LARGE: "78vh", FULL: "100vh" }[v.HEIGHT],
|
|
170
|
+
"--mwt-h-mobile": px(v.MEDIA_HEIGHT_MOBILE),
|
|
171
|
+
"--mwt-fit": String(v.MEDIA_FIT || "COVER").toLowerCase(),
|
|
172
|
+
"--mwt-radius": px(v.MEDIA_RADIUS),
|
|
173
|
+
|
|
174
|
+
// split
|
|
175
|
+
"--mwt-media-w": `${Number(v.MEDIA_WIDTH) || 50}%`,
|
|
176
|
+
"--mwt-gap": px(v.COLUMN_GAP),
|
|
177
|
+
"--mwt-gap-mobile": px(v.COLUMN_GAP_MOBILE),
|
|
178
|
+
"--mwt-valign": ALIGN[String(v.VERTICAL_ALIGN || "CENTER").toUpperCase()] ?? "center",
|
|
179
|
+
|
|
180
|
+
// overlay
|
|
181
|
+
"--mwt-ov-color": v.OVERLAY_COLOR,
|
|
182
|
+
"--mwt-ov-opacity": has(v.OVERLAY_OPACITY) ? String(Number(v.OVERLAY_OPACITY) / 100) : undefined,
|
|
183
|
+
"--mwt-ov-text": v.OVERLAY_TEXT_COLOR,
|
|
184
|
+
"--mwt-ov-y": spot[0],
|
|
185
|
+
"--mwt-ov-x": spot[1],
|
|
186
|
+
"--mwt-ov-max": px(v.OVERLAY_TEXT_MAX_WIDTH),
|
|
187
|
+
|
|
188
|
+
// text
|
|
189
|
+
"--mwt-text-align": TEXT_ALIGN[textAlign] ?? "start",
|
|
190
|
+
"--mwt-text-items": ITEMS_ALIGN[textAlign] ?? "flex-start",
|
|
191
|
+
"--mwt-text-max": px(v.TEXT_MAX_WIDTH),
|
|
192
|
+
"--mwt-text-bg": v.TEXT_BG_COLOR,
|
|
193
|
+
"--mwt-text-pt": px(v.TEXT_PADDING_TOP),
|
|
194
|
+
"--mwt-text-pr": px(v.TEXT_PADDING_RIGHT),
|
|
195
|
+
"--mwt-text-pb": px(v.TEXT_PADDING_BOTTOM),
|
|
196
|
+
"--mwt-text-pl": px(v.TEXT_PADDING_LEFT),
|
|
197
|
+
"--mwt-text-gap": px(v.TEXT_GAP),
|
|
198
|
+
"--mwt-h-fs": px(v.HEADING_TEXT_SIZE),
|
|
199
|
+
"--mwt-h-fs-mobile": px(v.HEADING_TEXT_SIZE_MOBILE),
|
|
200
|
+
"--mwt-h-fw": WEIGHTS[v.HEADING_TEXT_WEIGHT],
|
|
201
|
+
"--mwt-h-color": v.HEADING_TEXT_COLOR,
|
|
202
|
+
"--mwt-h-lh": has(v.HEADING_TEXT_LINE_HEIGHT) ? String(Number(v.HEADING_TEXT_LINE_HEIGHT) / 100) : undefined,
|
|
203
|
+
"--mwt-h-ls": has(v.HEADING_TEXT_LETTER_SPACING) ? `${Number(v.HEADING_TEXT_LETTER_SPACING) / 100}em` : undefined,
|
|
204
|
+
"--mwt-h-tt": CASES[v.HEADING_TEXT_TRANSFORM],
|
|
205
|
+
"--mwt-h-ff": v.HEADING_TEXT_FONT === "BODY" ? "var(--osi-font-body)" : undefined,
|
|
206
|
+
"--mwt-b-fs": px(v.BODY_SIZE),
|
|
207
|
+
"--mwt-b-color": v.BODY_COLOR,
|
|
208
|
+
"--mwt-eyebrow-color": v.EYEBROW_COLOR,
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const rootClasses = computed(() => [
|
|
213
|
+
`osi-mwt--${layout.value.toLowerCase().replace("_", "-")}`,
|
|
214
|
+
{
|
|
215
|
+
"osi-mwt--media-end": s.value.MEDIA_POSITION === "RIGHT",
|
|
216
|
+
"osi-mwt--text-first-mobile": !!s.value.MOBILE_TEXT_FIRST,
|
|
217
|
+
"osi-mwt--has-height":
|
|
218
|
+
has(s.value.MEDIA_HEIGHT) || has(s.value.MEDIA_HEIGHT_MOBILE) ||
|
|
219
|
+
["SMALL", "MEDIUM", "LARGE", "FULL"].includes(s.value.HEIGHT),
|
|
220
|
+
"osi-mwt--natural": String(s.value.MEDIA_RATIO || "") === "NATURAL",
|
|
221
|
+
"osi-mwt--no-media": !hasMedia.value,
|
|
222
|
+
// A colour scheme picked on the section: its text colour drives the overlay
|
|
223
|
+
// text too, instead of the default white.
|
|
224
|
+
"osi-mwt--scheme": !!s.value.COLOR_SCHEME,
|
|
225
|
+
},
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
const containerStyle = computed(() =>
|
|
229
|
+
fullBleed.value ? { maxWidth: "100%", marginInline: 0 } : innerStyle.value
|
|
230
|
+
);
|
|
231
|
+
/* Full bleed also ignores the section's side padding, so the picture meets
|
|
232
|
+
both edges of the screen. */
|
|
233
|
+
const sectionStyle = computed(() =>
|
|
234
|
+
fullBleed.value ? { ...shellStyle.value, paddingLeft: 0, paddingRight: 0 } : shellStyle.value
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
return { isMwt, s, t, shellStyle, innerStyle, customClass, heading, mediaUrl, posterUrl, isVideo, isMobile, has, px, compact, WEIGHTS, CASES, media, mediaSrc, mediaPath, isVideoMedia, videoAutoplay, videoLoop, videoMuted, videoControls, videoPoster, embedUrl, hasMedia, RATIOS, eyebrow, body, subheading, textBlocks, headingTag, eager, buttons, hasText, layout, fullBleed, mediaLink, NuxtLinkComponent, ALIGN, OVERLAY_SPOTS, TEXT_ALIGN, ITEMS_ALIGN, vars, rootClasses, containerStyle, sectionStyle };
|
|
238
|
+
}
|