@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,476 @@
|
|
|
1
|
+
import { SECTION_WIDTHS } from "../../composables/useSectionShell";
|
|
2
|
+
|
|
3
|
+
export default function useLogic({ props }) {
|
|
4
|
+
const isBar = computed(() => props.part === "bar");
|
|
5
|
+
// Utilities blocks' keys (FooterUtilitiesSection), to tag the parts.
|
|
6
|
+
const utilityBlocks = inject("osiUtilityBlocks", computed(() => ({})));
|
|
7
|
+
|
|
8
|
+
const settings = useThemeGroup("FOOTER");
|
|
9
|
+
const brand = useThemeGroup("BRAND");
|
|
10
|
+
/* ------------------------------------------------------------ appearance
|
|
11
|
+
* Like the header: a colour scheme with background / text / link overrides
|
|
12
|
+
* (empty = the scheme's). The bar under the footer (Utilities) follows the
|
|
13
|
+
* footer's scheme and overrides unless it has a scheme of its own.
|
|
14
|
+
*/
|
|
15
|
+
const pick = (key) => {
|
|
16
|
+
const own = props.own?.[key];
|
|
17
|
+
return own !== undefined && own !== null && own !== "" ? own : settings.value[key];
|
|
18
|
+
};
|
|
19
|
+
const barScheme = computed(() => (isBar.value ? pick("FOOTER_BOTTOM_COLOR_SCHEME") : null));
|
|
20
|
+
const scheme = useColorScheme(
|
|
21
|
+
// Nothing picked: the theme's Default scheme (useColorScheme's fallback),
|
|
22
|
+
// never a hardcoded SCHEME_1 — so changing it in Theme settings repaints
|
|
23
|
+
// the footer and the bar, with or without a Footer section.
|
|
24
|
+
computed(() => barScheme.value || pick("FOOTER_COLOR_SCHEME") || null)
|
|
25
|
+
);
|
|
26
|
+
const appearance = computed(() => {
|
|
27
|
+
// A bar with its own scheme starts from that scheme, not the footer's colours.
|
|
28
|
+
const inherit = !barScheme.value;
|
|
29
|
+
const background = isBar.value
|
|
30
|
+
? pick("FOOTER_BOTTOM_BACKGROUND") || (inherit ? pick("FOOTER_BACKGROUND") : null)
|
|
31
|
+
: pick("FOOTER_BACKGROUND");
|
|
32
|
+
const text = !isBar.value || inherit ? pick("FOOTER_TEXT_COLOR") : null;
|
|
33
|
+
const link = !isBar.value || inherit ? pick("FOOTER_LINK_COLOR") : null;
|
|
34
|
+
const hover = !isBar.value || inherit ? pick("FOOTER_LINK_HOVER_COLOR") : null;
|
|
35
|
+
return Object.fromEntries(Object.entries({
|
|
36
|
+
"--osi-bg": background || undefined,
|
|
37
|
+
"--osi-text": text || undefined,
|
|
38
|
+
"--osi-footer-link": link || undefined,
|
|
39
|
+
"--osi-footer-link-hover": hover || undefined,
|
|
40
|
+
}).filter(([, v]) => v !== undefined));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const { store, storeLogo } = useStorefront();
|
|
44
|
+
const { mediaUrl } = useMediaUrl();
|
|
45
|
+
const { useMenuByHandle } = useMenu();
|
|
46
|
+
const t = useT();
|
|
47
|
+
|
|
48
|
+
// Three fixed slots rather than a repeater: theme settings has no repeatable
|
|
49
|
+
// field type, and footer columns are menus the merchant already maintains.
|
|
50
|
+
// Empty handles resolve to empty menus and are filtered out below.
|
|
51
|
+
const menu1 = useMenuByHandle(computed(() => settings.value.FOOTER_MENU_1));
|
|
52
|
+
const menu2 = useMenuByHandle(computed(() => settings.value.FOOTER_MENU_2));
|
|
53
|
+
const menu3 = useMenuByHandle(computed(() => settings.value.FOOTER_MENU_3));
|
|
54
|
+
|
|
55
|
+
// Custom widths only take effect when FOOTER_COLUMN_WIDTH_MODE is CUSTOM;
|
|
56
|
+
// otherwise every column shares the row equally, exactly as before.
|
|
57
|
+
const columnWidthMode = computed(() => settings.value.FOOTER_COLUMN_WIDTH_MODE || "EQUAL");
|
|
58
|
+
function columnFlexBasis(widthSetting) {
|
|
59
|
+
if (columnWidthMode.value !== "CUSTOM") return null;
|
|
60
|
+
return `${widthSetting || 33}%`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const columns = computed(() =>
|
|
64
|
+
[
|
|
65
|
+
{
|
|
66
|
+
menu: menu1.value,
|
|
67
|
+
title: t(settings.value.FOOTER_MENU_1_TITLE),
|
|
68
|
+
flexBasis: columnFlexBasis(settings.value.FOOTER_MENU_1_WIDTH),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
menu: menu2.value,
|
|
72
|
+
title: t(settings.value.FOOTER_MENU_2_TITLE),
|
|
73
|
+
flexBasis: columnFlexBasis(settings.value.FOOTER_MENU_2_WIDTH),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
menu: menu3.value,
|
|
77
|
+
title: t(settings.value.FOOTER_MENU_3_TITLE),
|
|
78
|
+
flexBasis: columnFlexBasis(settings.value.FOOTER_MENU_3_WIDTH),
|
|
79
|
+
},
|
|
80
|
+
]
|
|
81
|
+
.filter((column) => column.menu.items.length)
|
|
82
|
+
.map((column) => ({ ...column, title: column.title || column.menu.title }))
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// FOOTER_LOGO overrides BRAND.LOGO for this section only; empty falls through
|
|
86
|
+
// to the brand logo and then the store's default logo, same chain the old
|
|
87
|
+
// markup used before the override field existed.
|
|
88
|
+
const logoSrc = computed(() =>
|
|
89
|
+
mediaUrl(settings.value.FOOTER_LOGO || brand.value.LOGO || storeLogo.value)
|
|
90
|
+
);
|
|
91
|
+
const logoPosition = computed(() => settings.value.FOOTER_LOGO_POSITION || "TOP_CENTER");
|
|
92
|
+
const logoAlignment = computed(() => {
|
|
93
|
+
const map = { LEFT: "flex-start", CENTER: "center", RIGHT: "flex-end" };
|
|
94
|
+
return map[settings.value.FOOTER_LOGO_ALIGNMENT] ?? "center";
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Mobile width falls back to the desktop width so old configs (which only
|
|
98
|
+
// ever set FOOTER_LOGO_WIDTH) render identically on every breakpoint.
|
|
99
|
+
const logoWidthVars = computed(() => {
|
|
100
|
+
const desktop = settings.value.FOOTER_LOGO_WIDTH || 64;
|
|
101
|
+
const mobile = settings.value.FOOTER_LOGO_WIDTH_MOBILE || desktop;
|
|
102
|
+
return {
|
|
103
|
+
"--osi-footer-logo-w": `${desktop}px`,
|
|
104
|
+
"--osi-footer-logo-w-mobile": `${mobile}px`,
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const showBrandDescription = computed(
|
|
109
|
+
() => settings.value.FOOTER_SHOW_BRAND_DESCRIPTION === true && !!t(settings.value.FOOTER_BRAND_DESCRIPTION)
|
|
110
|
+
);
|
|
111
|
+
// Independent from FOOTER_LOGO_ALIGNMENT: that one only exists for TOP_CENTER
|
|
112
|
+
// (it justifies the logo row), while the description renders in all three
|
|
113
|
+
// logo positions and needs its own alignment regardless of where the logo sits.
|
|
114
|
+
const DESCRIPTION_ALIGNMENTS = { LEFT: "left", CENTER: "center", RIGHT: "right" };
|
|
115
|
+
const brandDescriptionStyle = computed(() => ({
|
|
116
|
+
maxWidth: `${settings.value.FOOTER_BRAND_DESCRIPTION_WIDTH || 32}ch`,
|
|
117
|
+
textAlign: DESCRIPTION_ALIGNMENTS[settings.value.FOOTER_BRAND_DESCRIPTION_ALIGNMENT] || "left",
|
|
118
|
+
// Centers the box itself (not just the text inside it) so a short
|
|
119
|
+
// description doesn't stay pinned to the left edge of its column.
|
|
120
|
+
marginInline: settings.value.FOOTER_BRAND_DESCRIPTION_ALIGNMENT === "CENTER" ? "auto" : undefined,
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
const footerText = computed(() => t(settings.value.FOOTER_TEXT));
|
|
124
|
+
const copyright = computed(
|
|
125
|
+
() =>
|
|
126
|
+
t(settings.value.FOOTER_COPYRIGHT) ||
|
|
127
|
+
`© ${new Date().getFullYear()} ${store.value?.name ?? ""}`
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Guarded rather than assumed: StoreSerializer was trimmed to identity/theme
|
|
131
|
+
// fields, so these may not be on the bootstrap payload. If they aren't,
|
|
132
|
+
// the relevant block renders nothing instead of throwing.
|
|
133
|
+
// Socials live in useFooterSocials + FooterSocials.vue. SiteFooter only needs
|
|
134
|
+
// the count and the placement, because those decide whether a column or a
|
|
135
|
+
// bottom-bar zone renders at all.
|
|
136
|
+
const {
|
|
137
|
+
socials,
|
|
138
|
+
enabled: socialsEnabled,
|
|
139
|
+
position: socialPosition,
|
|
140
|
+
} = useFooterSocials();
|
|
141
|
+
|
|
142
|
+
const paymentIcons = computed(() => store.value?.payment_icons ?? []);
|
|
143
|
+
const languages = computed(() => store.value?.languages ?? []);
|
|
144
|
+
const countries = computed(() => store.value?.countries ?? []);
|
|
145
|
+
const currentLanguage = computed(() => store.value?.language ?? languages.value[0]);
|
|
146
|
+
const currentCountry = computed(() => store.value?.country ?? countries.value[0]);
|
|
147
|
+
|
|
148
|
+
// All "show X" booleans below default to true in the schema, so only an
|
|
149
|
+
// explicit `false` should hide them — undefined (an unsaved setting) must
|
|
150
|
+
// still fall through to the schema default, not be treated as "off".
|
|
151
|
+
const showSocial = socialsEnabled;
|
|
152
|
+
const socialAlignment = computed(() => {
|
|
153
|
+
const map = { LEFT: "flex-start", CENTER: "center", RIGHT: "flex-end" };
|
|
154
|
+
return map[settings.value.FOOTER_SOCIAL_ALIGNMENT] ?? "flex-start";
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const showNewsletter = computed(() => settings.value.FOOTER_SHOW_NEWSLETTER !== false);
|
|
158
|
+
const newsletterOnStart = computed(() => (settings.value.FOOTER_NEWSLETTER_POSITION || "START") === "START");
|
|
159
|
+
const newsletterStacked = computed(() => settings.value.FOOTER_NEWSLETTER_LAYOUT === "STACKED");
|
|
160
|
+
const newsletterAlignment = computed(() => (settings.value.FOOTER_NEWSLETTER_ALIGNMENT || "LEFT").toLowerCase());
|
|
161
|
+
const showConsent = computed(() => settings.value.FOOTER_NEWSLETTER_SHOW_CONSENT !== false);
|
|
162
|
+
|
|
163
|
+
// Local-only "did it submit" state — the schema's success message is just
|
|
164
|
+
// copy; wiring the form to a real subscribe endpoint is unrelated to this
|
|
165
|
+
// settings pass, so submitting just reveals the message as an acknowledgement.
|
|
166
|
+
const newsletterSubmitted = ref(false);
|
|
167
|
+
function handleNewsletterSubmit() {
|
|
168
|
+
newsletterSubmitted.value = true;
|
|
169
|
+
}
|
|
170
|
+
const newsletterSuccessMessage = computed(
|
|
171
|
+
() => t(settings.value.FOOTER_NEWSLETTER_SUCCESS_MESSAGE) || "Thanks for subscribing!"
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const showPaymentIcons = computed(() => settings.value.FOOTER_SHOW_PAYMENT_ICONS !== false);
|
|
175
|
+
const showLanguageSelector = computed(() => settings.value.FOOTER_SHOW_LANGUAGE_SELECTOR !== false);
|
|
176
|
+
const showCountrySelector = computed(() => settings.value.FOOTER_SHOW_COUNTRY_SELECTOR !== false);
|
|
177
|
+
|
|
178
|
+
// --------------------------------------------------------------- containers
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Inner: max-width and centring only.
|
|
182
|
+
*
|
|
183
|
+
* The side gutter belongs on the footer root, not here. useSectionShell puts
|
|
184
|
+
* paddingInline on the shell and maxWidth on the inner; the footer had both on
|
|
185
|
+
* the inner, which makes its usable width maxWidth minus twice the gutter while
|
|
186
|
+
* a section directly above it gets the full maxWidth. That 32px is the
|
|
187
|
+
* misalignment between the two.
|
|
188
|
+
*
|
|
189
|
+
* SECTION_WIDTHS comes from useSectionShell rather than a local copy — the
|
|
190
|
+
* footer's own map had NARROW at calc(page-width - 320px) against the sections'
|
|
191
|
+
* flat 720px, so the same enum value produced two different widths.
|
|
192
|
+
*/
|
|
193
|
+
const containerStyle = computed(() => ({
|
|
194
|
+
maxWidth: SECTION_WIDTHS[settings.value.FOOTER_CONTAINER] ?? SECTION_WIDTHS.NORMAL,
|
|
195
|
+
marginInline: "auto",
|
|
196
|
+
}));
|
|
197
|
+
|
|
198
|
+
const footerStyle = computed(() => ({
|
|
199
|
+
...scheme.value.style,
|
|
200
|
+
...appearance.value,
|
|
201
|
+
// Side gutter on the root, matching useSectionShell's shellStyle.
|
|
202
|
+
paddingInlineStart: `${settings.value.FOOTER_CONTAINER_PADDING_X ?? 16}px`,
|
|
203
|
+
paddingInlineEnd: `${settings.value.FOOTER_CONTAINER_PADDING_X ?? 16}px`,
|
|
204
|
+
"--osi-footer-pt": `${settings.value.FOOTER_PADDING_TOP ?? 64}px`,
|
|
205
|
+
"--osi-footer-pb": `${settings.value.FOOTER_PADDING_BOTTOM ?? 32}px`,
|
|
206
|
+
// The bottom bar needs vertical padding independent of FOOTER_PADDING_BOTTOM:
|
|
207
|
+
// that one is the gap below the whole footer, and base.json sets it to 0
|
|
208
|
+
// while still wanting the copyright row to breathe.
|
|
209
|
+
"--osi-footer-bar-py": `${settings.value.FOOTER_BOTTOM_PADDING_Y ?? 16}px`,
|
|
210
|
+
// Drives .osi-footer-inner and .osi-footer-main, which used to hardcode
|
|
211
|
+
// gap: 2rem regardless of what a merchant set here.
|
|
212
|
+
"--osi-footer-section-gap": `${settings.value.FOOTER_SECTION_GAP ?? 32}px`,
|
|
213
|
+
"--osi-footer-col-gap": `${settings.value.FOOTER_COLUMN_GAP ?? 64}px`,
|
|
214
|
+
"--osi-footer-col-row-gap": `${settings.value.FOOTER_COLUMN_ROW_GAP ?? 16}px`,
|
|
215
|
+
"--osi-footer-link-gap": `${settings.value.FOOTER_LINK_GAP ?? 12}px`,
|
|
216
|
+
"--osi-footer-social-gap": `${settings.value.FOOTER_SOCIAL_GAP ?? 16}px`,
|
|
217
|
+
"--osi-footer-payment-icon-size": `${settings.value.FOOTER_PAYMENT_ICON_SIZE || 20}px`,
|
|
218
|
+
"--osi-footer-payment-gap": `${settings.value.FOOTER_PAYMENT_ICON_GAP ?? 8}px`,
|
|
219
|
+
...logoWidthVars.value,
|
|
220
|
+
// The divider marks the boundary between page content and the footer's
|
|
221
|
+
// top content area. With .osi-footer-inner hidden (see hasFooterContent),
|
|
222
|
+
// that boundary doesn't exist — the border would just sit directly above
|
|
223
|
+
// the bottom bar instead, which isn't what FOOTER_SHOW_DIVIDER means.
|
|
224
|
+
borderTop: !hasFooterContent.value || settings.value.FOOTER_SHOW_DIVIDER === false
|
|
225
|
+
? "none"
|
|
226
|
+
: pick("FOOTER_DIVIDER_COLOR")
|
|
227
|
+
? `1px solid ${pick("FOOTER_DIVIDER_COLOR")}`
|
|
228
|
+
: undefined,
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
const columnsAlignment = computed(() => {
|
|
232
|
+
const map = { START: "flex-start", CENTER: "center", END: "flex-end", SPREAD: "space-between" };
|
|
233
|
+
return map[settings.value.FOOTER_COLUMNS_ALIGNMENT] ?? "flex-end";
|
|
234
|
+
});
|
|
235
|
+
const columnsVerticalAlignment = computed(() => {
|
|
236
|
+
const map = { TOP: "flex-start", CENTER: "center", BOTTOM: "flex-end" };
|
|
237
|
+
return map[settings.value.FOOTER_COLUMNS_VERTICAL_ALIGNMENT] ?? "flex-start";
|
|
238
|
+
});
|
|
239
|
+
const columnTextAlignment = computed(() => (settings.value.FOOTER_COLUMN_TEXT_ALIGNMENT || "LEFT").toLowerCase());
|
|
240
|
+
|
|
241
|
+
const JUSTIFY = { START: "flex-start", CENTER: "center", END: "flex-end", SPREAD: "space-between" };
|
|
242
|
+
// One per line: SPREAD has nothing to spread, so it centres.
|
|
243
|
+
const ALIGN = { START: "flex-start", CENTER: "center", END: "flex-end", SPREAD: "center" };
|
|
244
|
+
const TEXT = { START: "start", CENTER: "center", END: "end", SPREAD: "center" };
|
|
245
|
+
const bottomAlignment = computed(() => JUSTIFY[settings.value.FOOTER_BOTTOM_ALIGNMENT] ?? "center");
|
|
246
|
+
const showBottomBarDivider = computed(() => settings.value.FOOTER_SHOW_BOTTOM_BAR_DIVIDER !== false);
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Bottom bar arrangement.
|
|
251
|
+
*
|
|
252
|
+
* INLINE one flex row, distributed by FOOTER_BOTTOM_ALIGNMENT. What
|
|
253
|
+
* every existing store renders today, unchanged.
|
|
254
|
+
* THREE_ZONE copyright and powered-by at the start, policy links centred,
|
|
255
|
+
* socials at the end — the Shopify arrangement.
|
|
256
|
+
*
|
|
257
|
+
* INLINE stays the schema default so no saved theme changes appearance; the
|
|
258
|
+
* preset opts into THREE_ZONE.
|
|
259
|
+
*/
|
|
260
|
+
const bottomThreeZone = computed(
|
|
261
|
+
() => String(settings.value.FOOTER_BOTTOM_LAYOUT ?? "INLINE").toUpperCase() === "THREE_ZONE"
|
|
262
|
+
);
|
|
263
|
+
// STACKED: each part on its own line, aligned by FOOTER_BOTTOM_ALIGNMENT.
|
|
264
|
+
const bottomStacked = computed(
|
|
265
|
+
() => String(settings.value.FOOTER_BOTTOM_LAYOUT ?? "").toUpperCase() === "STACKED"
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const px = (v) => (v === "" || v == null || !Number.isFinite(Number(v)) ? undefined : `${Number(v)}px`);
|
|
269
|
+
|
|
270
|
+
/** The bar's own look (Utilities section) and its phone alignment. */
|
|
271
|
+
const barVars = computed(() => {
|
|
272
|
+
const s = settings.value;
|
|
273
|
+
const align = String(s.FOOTER_BOTTOM_ALIGNMENT || "CENTER").toUpperCase();
|
|
274
|
+
const mobile = String(s.FOOTER_BOTTOM_MOBILE_ALIGNMENT || "CENTER").toUpperCase();
|
|
275
|
+
return Object.fromEntries(Object.entries({
|
|
276
|
+
"--bar-gap": px(s.FOOTER_BOTTOM_GAP),
|
|
277
|
+
"--bar-fs": px(s.FOOTER_BOTTOM_TEXT_SIZE),
|
|
278
|
+
"--bar-color": s.FOOTER_BOTTOM_TEXT_COLOR || undefined,
|
|
279
|
+
// A chosen colour is shown as chosen, not faded.
|
|
280
|
+
"--bar-opacity": s.FOOTER_BOTTOM_TEXT_COLOR ? "1" : undefined,
|
|
281
|
+
"--bar-align": ALIGN[align],
|
|
282
|
+
"--bar-text": TEXT[align],
|
|
283
|
+
"--bar-m-justify": JUSTIFY[mobile],
|
|
284
|
+
"--bar-m-align": ALIGN[mobile],
|
|
285
|
+
"--bar-m-text": TEXT[mobile],
|
|
286
|
+
}).filter(([, v]) => v !== undefined));
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
/** Policy links block: its own spacing, separator, underline, size, colour. */
|
|
290
|
+
const policyClasses = computed(() => {
|
|
291
|
+
const s = settings.value;
|
|
292
|
+
return [
|
|
293
|
+
`pol-sep-${String(s.FOOTER_POLICY_SEPARATOR || "NONE").toLowerCase()}`,
|
|
294
|
+
`pol-underline-${String(s.FOOTER_POLICY_UNDERLINE || "HOVER").toLowerCase()}`,
|
|
295
|
+
{ "pol-stack-mobile": s.FOOTER_POLICY_MOBILE_INLINE === false },
|
|
296
|
+
];
|
|
297
|
+
});
|
|
298
|
+
const policyStyle = computed(() => {
|
|
299
|
+
const s = settings.value;
|
|
300
|
+
return Object.fromEntries(Object.entries({
|
|
301
|
+
"--pol-gap": px(s.FOOTER_POLICY_GAP),
|
|
302
|
+
"--pol-fs": px(s.FOOTER_POLICY_TEXT_SIZE),
|
|
303
|
+
"--pol-color": s.FOOTER_POLICY_COLOR || undefined,
|
|
304
|
+
order: isBar.value ? utilityBlocks.value.order?.policies : undefined,
|
|
305
|
+
}).filter(([, v]) => v !== undefined && v !== -1));
|
|
306
|
+
});
|
|
307
|
+
const copyrightStyle = computed(() => {
|
|
308
|
+
const s = settings.value;
|
|
309
|
+
return Object.fromEntries(Object.entries({
|
|
310
|
+
fontSize: px(s.FOOTER_COPYRIGHT_TEXT_SIZE),
|
|
311
|
+
color: s.FOOTER_COPYRIGHT_COLOR || undefined,
|
|
312
|
+
}).filter(([, v]) => v !== undefined));
|
|
313
|
+
});
|
|
314
|
+
/** Utilities: parts in block order. */
|
|
315
|
+
const orderStyle = (part) => {
|
|
316
|
+
const n = isBar.value ? utilityBlocks.value.order?.[part] : undefined;
|
|
317
|
+
return n === undefined || n < 0 ? undefined : { order: n };
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* The bottom bar is a sibling of .osi-footer-inner, not a child, so it doesn't
|
|
322
|
+
* inherit the container. It takes the same containerStyle so its content —
|
|
323
|
+
* and its top border — line up with the columns above rather than running the
|
|
324
|
+
* full width of the footer.
|
|
325
|
+
*
|
|
326
|
+
* Kept a sibling rather than moved inside .osi-footer-inner because that
|
|
327
|
+
* element owns FOOTER_PADDING_BOTTOM; putting the bar in it would place the
|
|
328
|
+
* footer's bottom padding above the copyright row instead of below it.
|
|
329
|
+
*
|
|
330
|
+
* justifyContent only applies to INLINE — the three-zone grid places its own
|
|
331
|
+
* children, so handing it a flex property would do nothing at best.
|
|
332
|
+
*/
|
|
333
|
+
const barStyle = computed(() => ({
|
|
334
|
+
...containerStyle.value,
|
|
335
|
+
...barVars.value,
|
|
336
|
+
...(bottomThreeZone.value || bottomStacked.value ? {} : { justifyContent: bottomAlignment.value }),
|
|
337
|
+
}));
|
|
338
|
+
|
|
339
|
+
const showPoweredBy = computed(() => settings.value.FOOTER_SHOW_POWERED_BY !== false);
|
|
340
|
+
const poweredByText = computed(() => t(settings.value.FOOTER_POWERED_BY_TEXT));
|
|
341
|
+
// Plain text between the copyright and the link — not part of the link.
|
|
342
|
+
const poweredByPrefix = computed(() => {
|
|
343
|
+
const v = settings.value.FOOTER_POWERED_BY_PREFIX;
|
|
344
|
+
return v === undefined || v === null ? ", " : String(v);
|
|
345
|
+
});
|
|
346
|
+
// Copyright block: space between the copyright and "powered by" (default 0).
|
|
347
|
+
const copyrightGap = computed(() => {
|
|
348
|
+
const n = Number(settings.value.FOOTER_COPYRIGHT_GAP);
|
|
349
|
+
return { columnGap: `${Number.isFinite(n) ? n : 0}px` };
|
|
350
|
+
});
|
|
351
|
+
const poweredByUrl = computed(() => settings.value.FOOTER_POWERED_BY_URL || "");
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Policy links as four fixed label/URL pairs, not a menu.
|
|
355
|
+
*
|
|
356
|
+
* The footer columns above are menus because a merchant curates those and their
|
|
357
|
+
* length varies. Policy links are a short fixed set that every store has, and
|
|
358
|
+
* requiring a merchant to build a menu before their terms link appears is a
|
|
359
|
+
* setup step with no upside. Same reasoning as the three fixed column slots:
|
|
360
|
+
* theme settings has no repeatable field type.
|
|
361
|
+
*
|
|
362
|
+
* A slot with a label but no URL is dropped rather than rendered as dead text.
|
|
363
|
+
*/
|
|
364
|
+
const POLICY_SLOTS = [1, 2, 3, 4];
|
|
365
|
+
|
|
366
|
+
const policyLinks = computed(() => {
|
|
367
|
+
// Utilities: the Policy links block is the switch.
|
|
368
|
+
if (!isBar.value && settings.value.FOOTER_SHOW_POLICY_LINKS === false) return [];
|
|
369
|
+
return POLICY_SLOTS.map((n) => ({
|
|
370
|
+
key: `policy-${n}`,
|
|
371
|
+
label: t(settings.value[`FOOTER_POLICY_${n}_LABEL`]),
|
|
372
|
+
url: settings.value[`FOOTER_POLICY_${n}_URL`] || "",
|
|
373
|
+
})).filter((link) => link.label && link.url);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Placement is FOOTER_SOCIAL_POSITION's decision, always.
|
|
378
|
+
*
|
|
379
|
+
* Nothing double-renders because the four positions are mutually exclusive —
|
|
380
|
+
* that's what makes an enum the right shape for this. An earlier version had
|
|
381
|
+
* THREE_ZONE seize the socials to avoid a double render, which prevented the
|
|
382
|
+
* duplicate but also silently discarded the merchant's OWN_COLUMN choice: two
|
|
383
|
+
* settings fighting over one decision, with the winner invisible in the panel.
|
|
384
|
+
*
|
|
385
|
+
* So in the three-zone bar the end zone carries the socials only when the
|
|
386
|
+
* merchant put them there. Otherwise it carries the payment icons alone, and
|
|
387
|
+
* the socials render wherever they were assigned.
|
|
388
|
+
*/
|
|
389
|
+
// The Utilities section's Social media links block; the old fixed footer's
|
|
390
|
+
// "with the copyright" position.
|
|
391
|
+
const socialsInBar = computed(() =>
|
|
392
|
+
isBar.value
|
|
393
|
+
? !!settings.value.FOOTER_UTIL_SOCIAL
|
|
394
|
+
: props.part === "all" && showSocial.value && socialPosition.value === "WITH_COPYRIGHT"
|
|
395
|
+
);
|
|
396
|
+
const socialsInColumns = computed(
|
|
397
|
+
() =>
|
|
398
|
+
showSocial.value &&
|
|
399
|
+
(socialPosition.value === "OWN_COLUMN" || socialPosition.value === "UNDER_MENU")
|
|
400
|
+
);
|
|
401
|
+
const socialsUnderNewsletter = computed(
|
|
402
|
+
() => showSocial.value && socialPosition.value === "UNDER_NEWSLETTER"
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
// FOOTER_PADDING_TOP/BOTTOM exist to space real content away from the edges
|
|
406
|
+
// of .osi-footer-inner. With the newsletter turned off, no menus assigned,
|
|
407
|
+
// no logo, no footer text, and no locale selector, that padding has nothing
|
|
408
|
+
// to space out — it just renders as blank area above the bottom bar. Gate
|
|
409
|
+
// the whole block on there being something to show, same as every other
|
|
410
|
+
// piece of the footer is already individually gated.
|
|
411
|
+
const hasFooterContent = computed(() =>
|
|
412
|
+
Boolean(
|
|
413
|
+
props.tree?.children?.length ||
|
|
414
|
+
(logoSrc.value && logoPosition.value === "TOP_CENTER") ||
|
|
415
|
+
showNewsletter.value ||
|
|
416
|
+
columns.value.length ||
|
|
417
|
+
socialsInColumns.value ||
|
|
418
|
+
(logoSrc.value && logoPosition.value !== "TOP_CENTER") ||
|
|
419
|
+
footerText.value ||
|
|
420
|
+
(showLanguageSelector.value && (currentLanguage.value || currentCountry.value))
|
|
421
|
+
)
|
|
422
|
+
);
|
|
423
|
+
// ------------------------------------------------------- mobile accordion
|
|
424
|
+
//
|
|
425
|
+
// Desktop is unaffected either way: the accordion state only changes which
|
|
426
|
+
// CSS rules apply under the mobile breakpoint (see <style>), and open state
|
|
427
|
+
// is tracked per column index rather than per menu handle so a column
|
|
428
|
+
// staying open/closed doesn't depend on which menu happens to be assigned to it.
|
|
429
|
+
const mobileAccordion = computed(() => settings.value.FOOTER_MOBILE_COLUMN_LAYOUT === "ACCORDION");
|
|
430
|
+
const openColumns = ref(
|
|
431
|
+
settings.value.FOOTER_MOBILE_ACCORDION_EXPAND_FIRST === true ? { 0: true } : {}
|
|
432
|
+
);
|
|
433
|
+
function toggleColumn(index) {
|
|
434
|
+
openColumns.value = { ...openColumns.value, [index]: !openColumns.value[index] };
|
|
435
|
+
}
|
|
436
|
+
function isColumnOpen(index) {
|
|
437
|
+
return !!openColumns.value[index];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ---------------------------------------------------------- text styling
|
|
441
|
+
//
|
|
442
|
+
// Mirrors what text_style() emits in the schema (SIZE / SIZE_MOBILE / WEIGHT /
|
|
443
|
+
// COLOR / LINE_HEIGHT / LETTER_SPACING / FONT) into CSS custom properties, so
|
|
444
|
+
// the two typography sets (heading, link) share one mapping instead of two
|
|
445
|
+
// near-identical computeds. TRANSFORM is applied as an inline style rather
|
|
446
|
+
// than a CSS var so its default can differ per prefix (headings default to
|
|
447
|
+
// uppercase, links don't) without a third variable.
|
|
448
|
+
|
|
449
|
+
const FONT_WEIGHTS = { REGULAR: 400, MEDIUM: 500, SEMIBOLD: 600, BOLD: 700 };
|
|
450
|
+
const FONT_FAMILIES = { HEADING: "var(--osi-font-heading)", BODY: "var(--osi-font-body)" };
|
|
451
|
+
const TEXT_TRANSFORMS = {
|
|
452
|
+
NONE: "none",
|
|
453
|
+
UPPERCASE: "uppercase",
|
|
454
|
+
LOWERCASE: "lowercase",
|
|
455
|
+
CAPITALIZE: "capitalize",
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
function textStyleVars(prefix, defaultTransform) {
|
|
459
|
+
const s = settings.value;
|
|
460
|
+
return {
|
|
461
|
+
"--osi-fs": `${s[`${prefix}_SIZE`] ?? 14}px`,
|
|
462
|
+
"--osi-fs-mobile": `${s[`${prefix}_SIZE_MOBILE`] || s[`${prefix}_SIZE`] || 14}px`,
|
|
463
|
+
"--osi-fw": FONT_WEIGHTS[s[`${prefix}_WEIGHT`]] ?? 400,
|
|
464
|
+
"--osi-lh": `${(s[`${prefix}_LINE_HEIGHT`] ?? 130) / 100}`,
|
|
465
|
+
"--osi-ls": `${(s[`${prefix}_LETTER_SPACING`] ?? 0) / 100}em`,
|
|
466
|
+
"--osi-ff": FONT_FAMILIES[s[`${prefix}_FONT`]] ?? "inherit",
|
|
467
|
+
color: s[`${prefix}_COLOR`] || undefined,
|
|
468
|
+
textTransform: TEXT_TRANSFORMS[s[`${prefix}_TRANSFORM`]] ?? TEXT_TRANSFORMS[defaultTransform],
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const headingTextStyle = computed(() => textStyleVars("FOOTER_HEADING", "UPPERCASE"));
|
|
473
|
+
const linkTextStyle = computed(() => textStyleVars("FOOTER_LINK", "NONE"));
|
|
474
|
+
|
|
475
|
+
return { isBar, utilityBlocks, settings, brand, pick, barScheme, scheme, appearance, store, storeLogo, mediaUrl, useMenuByHandle, t, menu1, menu2, menu3, columnWidthMode, columnFlexBasis, columns, logoSrc, logoPosition, logoAlignment, logoWidthVars, showBrandDescription, DESCRIPTION_ALIGNMENTS, brandDescriptionStyle, footerText, copyright, socials, socialsEnabled, socialPosition, paymentIcons, languages, countries, currentLanguage, currentCountry, showSocial, socialAlignment, showNewsletter, newsletterOnStart, newsletterStacked, newsletterAlignment, showConsent, newsletterSubmitted, handleNewsletterSubmit, newsletterSuccessMessage, showPaymentIcons, showLanguageSelector, showCountrySelector, containerStyle, footerStyle, columnsAlignment, columnsVerticalAlignment, columnTextAlignment, JUSTIFY, ALIGN, TEXT, bottomAlignment, showBottomBarDivider, bottomThreeZone, bottomStacked, px, barVars, policyClasses, policyStyle, copyrightStyle, orderStyle, barStyle, showPoweredBy, poweredByText, poweredByPrefix, copyrightGap, poweredByUrl, POLICY_SLOTS, policyLinks, socialsInBar, socialsInColumns, socialsUnderNewsletter, hasFooterContent, mobileAccordion, openColumns, toggleColumn, isColumnOpen, FONT_WEIGHTS, FONT_FAMILIES, TEXT_TRANSFORMS, textStyleVars, headingTextStyle, linkTextStyle };
|
|
476
|
+
}
|