@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,171 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FONT_FAMILIES,
|
|
3
|
+
TEXT_TRANSFORMS,
|
|
4
|
+
color,
|
|
5
|
+
compact,
|
|
6
|
+
letterSpacing,
|
|
7
|
+
px,
|
|
8
|
+
shadow,
|
|
9
|
+
weight,
|
|
10
|
+
} from "#osi/theme/controls";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Theme settings -> CSS custom properties.
|
|
14
|
+
*
|
|
15
|
+
* Every component styles itself from these variables. Nothing reads
|
|
16
|
+
* theme.HEADING_FONT directly — that's what lets a merchant change a font and
|
|
17
|
+
* have the whole storefront follow without a single component knowing.
|
|
18
|
+
*
|
|
19
|
+
* Spread onto the root element in app.vue, together with `rootClasses`.
|
|
20
|
+
*
|
|
21
|
+
* FIXED FONTS
|
|
22
|
+
*
|
|
23
|
+
* A hand-built store (stores/<name>) can pin its fonts in app.config.ts:
|
|
24
|
+
* fonts: { heading: 'Avenir', body: 'Avenir' }
|
|
25
|
+
* A pinned font wins over the merchant's HEADING_FONT / BODY_FONT, and isn't
|
|
26
|
+
* loaded from Google — the store ships it itself (@font-face in its CSS).
|
|
27
|
+
*
|
|
28
|
+
* BUTTONS / INPUTS
|
|
29
|
+
*
|
|
30
|
+
* The --osi-button-* and --osi-input-* sets are what every cart, login,
|
|
31
|
+
* checkout, account and section control reads (assets/css/osi-controls.css).
|
|
32
|
+
* Colours are only emitted when the merchant set one: an unset button colour
|
|
33
|
+
* has to keep following the section's colour scheme (--osi-btn-bg), and it
|
|
34
|
+
* can't if the theme pins it here.
|
|
35
|
+
*
|
|
36
|
+
* The three enum settings (button style, hover effect, field style, label
|
|
37
|
+
* position) can't be variables — CSS can't branch on one — so they become
|
|
38
|
+
* classes on the root instead.
|
|
39
|
+
*/
|
|
40
|
+
export function useThemeTokens() {
|
|
41
|
+
const { theme, store } = useStorefront();
|
|
42
|
+
const fixedFonts = useAppConfig().fonts ?? {};
|
|
43
|
+
|
|
44
|
+
// Pages outside the section renderer (login, checkout, account, the cart
|
|
45
|
+
// drawer) never get a section's scheme variables, so the default scheme is
|
|
46
|
+
// published on the root too. Sections still override it on their own root.
|
|
47
|
+
const defaultScheme = useColorScheme(computed(() => null));
|
|
48
|
+
|
|
49
|
+
const tokens = computed(() => {
|
|
50
|
+
const t = theme.value ?? {};
|
|
51
|
+
const s = store.value ?? {};
|
|
52
|
+
|
|
53
|
+
const headingFont = fixedFonts.heading ?? t.HEADING_FONT ?? "Poppins";
|
|
54
|
+
const bodyFont = fixedFonts.body ?? t.BODY_FONT ?? "DM Sans";
|
|
55
|
+
|
|
56
|
+
return compact({
|
|
57
|
+
...defaultScheme.value.style,
|
|
58
|
+
|
|
59
|
+
"--primary-color": s.primary_color ?? "#000000",
|
|
60
|
+
"--secondary-color": s.secondary_color ?? "#f3f4f6",
|
|
61
|
+
"--osi-primary-color": s.primary_color ?? "#000000",
|
|
62
|
+
"--osi-secondary-color": s.secondary_color ?? "#f3f4f6",
|
|
63
|
+
|
|
64
|
+
"--osi-font-heading": `"${headingFont}", system-ui, sans-serif`,
|
|
65
|
+
"--osi-font-body": `"${bodyFont}", system-ui, sans-serif`,
|
|
66
|
+
"--osi-heading-scale": (t.HEADING_SCALE ?? 100) / 100,
|
|
67
|
+
"--osi-body-size": `${((t.BODY_SCALE ?? 100) / 100) * 16}px`,
|
|
68
|
+
|
|
69
|
+
"--osi-page-width": `${t.PAGE_WIDTH ?? 1400}px`,
|
|
70
|
+
"--osi-section-gap": `${t.SECTION_SPACING ?? 48}px`,
|
|
71
|
+
"--osi-grid-gap": `${t.GRID_GAP ?? 24}px`,
|
|
72
|
+
|
|
73
|
+
"--osi-logo-width": `${t.LOGO_WIDTH ?? 120}px`,
|
|
74
|
+
|
|
75
|
+
// ------------------------------------------------------------ buttons
|
|
76
|
+
// Legacy names, still read by a few component stylesheets.
|
|
77
|
+
"--osi-btn-radius": px(t.BUTTON_RADIUS) ?? "4px",
|
|
78
|
+
"--osi-btn-border": px(t.BUTTON_BORDER_WIDTH) ?? "1px",
|
|
79
|
+
"--osi-btn-shadow": shadow(t.BUTTON_SHADOW, t.BUTTON_SHADOW_COLOR) ?? "none",
|
|
80
|
+
|
|
81
|
+
"--osi-button-radius": px(t.BUTTON_RADIUS) ?? "4px",
|
|
82
|
+
"--osi-button-border-w": px(t.BUTTON_BORDER_WIDTH) ?? "1px",
|
|
83
|
+
"--osi-button-shadow": shadow(t.BUTTON_SHADOW, t.BUTTON_SHADOW_COLOR) ?? "none",
|
|
84
|
+
"--osi-button-hover-shadow": shadow(t.BUTTON_HOVER_SHADOW, t.BUTTON_SHADOW_COLOR),
|
|
85
|
+
"--osi-button-pad-y": px(t.BUTTON_PADDING_Y) ?? "12px",
|
|
86
|
+
"--osi-button-pad-x": px(t.BUTTON_PADDING_X) ?? "28px",
|
|
87
|
+
"--osi-button-pad-y-mobile": px(t.BUTTON_PADDING_Y_MOBILE),
|
|
88
|
+
"--osi-button-pad-x-mobile": px(t.BUTTON_PADDING_X_MOBILE),
|
|
89
|
+
"--osi-button-fs": px(t.BUTTON_FONT_SIZE) ?? "14px",
|
|
90
|
+
"--osi-button-fw": weight(t.BUTTON_FONT_WEIGHT) ?? 500,
|
|
91
|
+
"--osi-button-ff": FONT_FAMILIES[t.BUTTON_FONT] ?? "inherit",
|
|
92
|
+
"--osi-button-tt": TEXT_TRANSFORMS[t.BUTTON_TRANSFORM] ?? "none",
|
|
93
|
+
"--osi-button-ls": letterSpacing(t.BUTTON_LETTER_SPACING) ?? "normal",
|
|
94
|
+
"--osi-button-bg": color(t.BUTTON_BG_COLOR),
|
|
95
|
+
"--osi-button-fg": color(t.BUTTON_TEXT_COLOR),
|
|
96
|
+
"--osi-button-border-color": color(t.BUTTON_BORDER_COLOR),
|
|
97
|
+
"--osi-button-hover-bg": color(t.BUTTON_HOVER_BG_COLOR),
|
|
98
|
+
"--osi-button-hover-fg": color(t.BUTTON_HOVER_TEXT_COLOR),
|
|
99
|
+
"--osi-button-hover-border-color": color(t.BUTTON_HOVER_BORDER_COLOR),
|
|
100
|
+
|
|
101
|
+
"--osi-button2-bg": color(t.SECONDARY_BUTTON_BG_COLOR),
|
|
102
|
+
"--osi-button2-fg": color(t.SECONDARY_BUTTON_TEXT_COLOR),
|
|
103
|
+
"--osi-button2-border-color": color(t.SECONDARY_BUTTON_BORDER_COLOR),
|
|
104
|
+
|
|
105
|
+
// ------------------------------------------------------------- inputs
|
|
106
|
+
"--osi-input-radius": px(t.INPUT_RADIUS) ?? "4px",
|
|
107
|
+
"--osi-input-border-w": px(t.INPUT_BORDER_WIDTH) ?? "1px",
|
|
108
|
+
"--osi-input-shadow": shadow(t.INPUT_SHADOW, t.INPUT_SHADOW_COLOR) ?? "none",
|
|
109
|
+
"--osi-input-pad-y": px(t.INPUT_PADDING_Y) ?? "10px",
|
|
110
|
+
"--osi-input-pad-x": px(t.INPUT_PADDING_X) ?? "12px",
|
|
111
|
+
"--osi-input-pad-y-mobile": px(t.INPUT_PADDING_Y_MOBILE),
|
|
112
|
+
"--osi-input-pad-x-mobile": px(t.INPUT_PADDING_X_MOBILE),
|
|
113
|
+
"--osi-input-fs": px(t.INPUT_FONT_SIZE) ?? "15px",
|
|
114
|
+
"--osi-input-fs-mobile": px(t.INPUT_FONT_SIZE_MOBILE),
|
|
115
|
+
"--osi-input-bg": color(t.INPUT_BG_COLOR),
|
|
116
|
+
"--osi-input-fg": color(t.INPUT_TEXT_COLOR),
|
|
117
|
+
"--osi-input-border-color": color(t.INPUT_BORDER_COLOR),
|
|
118
|
+
"--osi-input-focus": color(t.INPUT_FOCUS_COLOR),
|
|
119
|
+
"--osi-input-placeholder": color(t.INPUT_PLACEHOLDER_COLOR),
|
|
120
|
+
"--osi-input-error": color(t.INPUT_ERROR_COLOR),
|
|
121
|
+
"--osi-input-gap": px(t.INPUT_GAP),
|
|
122
|
+
|
|
123
|
+
"--osi-label-fs": px(t.INPUT_LABEL_SIZE),
|
|
124
|
+
"--osi-label-fw": weight(t.INPUT_LABEL_WEIGHT),
|
|
125
|
+
"--osi-label-tt": TEXT_TRANSFORMS[t.INPUT_LABEL_TRANSFORM],
|
|
126
|
+
"--osi-label-ls": letterSpacing(t.INPUT_LABEL_LETTER_SPACING),
|
|
127
|
+
"--osi-label-color": color(t.INPUT_LABEL_COLOR),
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const { controls, rootClasses } = useThemeControls();
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Fonts are arbitrary merchant-chosen strings, so they're loaded at runtime
|
|
135
|
+
* rather than bundled. If the store has an Arabic locale, load an Arabic
|
|
136
|
+
* subset too — Poppins has no Arabic glyphs and the fallback looks broken.
|
|
137
|
+
* Pinned fonts (see FIXED FONTS) are skipped: the store ships them itself.
|
|
138
|
+
*/
|
|
139
|
+
const fontLinks = computed(() => {
|
|
140
|
+
const t = theme.value ?? {};
|
|
141
|
+
const families = [
|
|
142
|
+
...new Set(
|
|
143
|
+
[
|
|
144
|
+
fixedFonts.heading ? null : t.HEADING_FONT,
|
|
145
|
+
fixedFonts.body ? null : t.BODY_FONT,
|
|
146
|
+
].filter(Boolean),
|
|
147
|
+
),
|
|
148
|
+
];
|
|
149
|
+
if (!families.length) return [];
|
|
150
|
+
const css2 = (spec) => `https://fonts.googleapis.com/css2?family=${spec}&display=swap`;
|
|
151
|
+
// Two stylesheets per family, never one shared request: Google answers
|
|
152
|
+
// 400 for the WHOLE request when a family lacks any listed weight (Bebas
|
|
153
|
+
// Neue has 400 only), which silently dropped every font. The plain one
|
|
154
|
+
// always loads (regular); the weighted one adds the rest where they
|
|
155
|
+
// exist and fails alone where they don't.
|
|
156
|
+
const sheets = families.flatMap((family) => {
|
|
157
|
+
const name = encodeURIComponent(family).replace(/%20/g, "+");
|
|
158
|
+
return [
|
|
159
|
+
{ rel: "stylesheet", href: css2(name), key: `osi-font-${family}` },
|
|
160
|
+
{ rel: "stylesheet", href: css2(`${name}:wght@300;400;500;600;700`), key: `osi-font-${family}-w` },
|
|
161
|
+
];
|
|
162
|
+
});
|
|
163
|
+
return [
|
|
164
|
+
{ rel: "preconnect", href: "https://fonts.googleapis.com", key: "osi-font-pc1" },
|
|
165
|
+
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "", key: "osi-font-pc2" },
|
|
166
|
+
...sheets,
|
|
167
|
+
];
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return { tokens, rootClasses, controls, fontLinks };
|
|
171
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import WishlistServices from "#osi/services/WishlistServices";
|
|
2
|
+
import { authBus } from "#osi/services/AuthBus";
|
|
3
|
+
import { analytics } from "#osi/utils/analytics";
|
|
4
|
+
import { useToast } from "vue-toastification";
|
|
5
|
+
|
|
6
|
+
export function useWishlist() {
|
|
7
|
+
const wishlistStatus = useState("wishlist-status", () => ({}));
|
|
8
|
+
const wishlistedItems = useState("wishlist-items", () => []);
|
|
9
|
+
|
|
10
|
+
const isLoggedIn = computed(() => authBus.isLoggedIn);
|
|
11
|
+
|
|
12
|
+
async function fetchWishlist() {
|
|
13
|
+
if (!isLoggedIn.value) return;
|
|
14
|
+
try {
|
|
15
|
+
const response = await WishlistServices.getWishlists();
|
|
16
|
+
wishlistedItems.value = response?.results || response || [];
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error("[useWishlist] Error fetching wishlist:", error);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function addToWishlist(product) {
|
|
23
|
+
const toast = useToast();
|
|
24
|
+
const productId = product.id;
|
|
25
|
+
|
|
26
|
+
if (!isLoggedIn.value) {
|
|
27
|
+
toast.error("Please log in to add items to your wishlist.");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Optimistic UI Update
|
|
32
|
+
const previousStatus = wishlistStatus.value[productId] ?? product.wishlisted;
|
|
33
|
+
wishlistStatus.value[productId] = !previousStatus;
|
|
34
|
+
product.wishlisted = !previousStatus;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
let response;
|
|
38
|
+
if (previousStatus) {
|
|
39
|
+
response = await WishlistServices.removeFromWishlist(productId);
|
|
40
|
+
toast.success("Product removed from wishlist");
|
|
41
|
+
} else {
|
|
42
|
+
response = await WishlistServices.addToWishlist(productId);
|
|
43
|
+
toast.success("Product added to wishlist");
|
|
44
|
+
analytics.addToWishlist(product);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (response && response.wishlisted !== undefined) {
|
|
48
|
+
wishlistStatus.value[productId] = response.wishlisted;
|
|
49
|
+
product.wishlisted = response.wishlisted;
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error("[useWishlist] Error updating wishlist:", error);
|
|
53
|
+
toast.error("An error occurred while updating your wishlist.");
|
|
54
|
+
wishlistStatus.value[productId] = previousStatus;
|
|
55
|
+
product.wishlisted = previousStatus;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
wishlistStatus,
|
|
61
|
+
wishlistedItems,
|
|
62
|
+
isLoggedIn,
|
|
63
|
+
fetchWishlist,
|
|
64
|
+
addToWishlist,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { withAsyncContext } from "vue";
|
|
2
|
+
import { Autoplay, Navigation, EffectFade } from "swiper/modules";
|
|
3
|
+
|
|
4
|
+
export default async function useLogic({ props }) {
|
|
5
|
+
let __temp, __restore;
|
|
6
|
+
|
|
7
|
+
const settings = useThemeGroup("ANNOUNCEMENT");
|
|
8
|
+
const scheme = useColorScheme(computed(() => settings.value.ANNOUNCEMENT_COLOR_SCHEME));
|
|
9
|
+
const { isMobile } = useLayoutShell();
|
|
10
|
+
const t = useT();
|
|
11
|
+
|
|
12
|
+
const px = (v) => (v === "" || v == null || !Number.isFinite(Number(v)) ? undefined : `${Number(v)}px`);
|
|
13
|
+
const WEIGHTS = { LIGHT: 300, REGULAR: 400, MEDIUM: 500, SEMIBOLD: 600, BOLD: 700 };
|
|
14
|
+
const weightOf = (v) => {
|
|
15
|
+
const n = Number(v);
|
|
16
|
+
return Number.isFinite(n) && n > 0 ? n : WEIGHTS[String(v || "").toUpperCase()];
|
|
17
|
+
};
|
|
18
|
+
const CASES = { NONE: "none", UPPERCASE: "uppercase", LOWERCASE: "lowercase", CAPITALIZE: "capitalize" };
|
|
19
|
+
|
|
20
|
+
const blockMessages = computed(() =>
|
|
21
|
+
(props.blocks ?? [])
|
|
22
|
+
.filter((b) => b.type === "ANNOUNCEMENT" && !b.disabled)
|
|
23
|
+
.map((b) => {
|
|
24
|
+
const v = b.settings ?? {};
|
|
25
|
+
const style = Object.fromEntries(Object.entries({
|
|
26
|
+
"--ann-fs": px(v.SIZE),
|
|
27
|
+
"--ann-fs-m": px(v.SIZE_MOBILE) ?? px(v.SIZE),
|
|
28
|
+
fontFamily: String(v.FONT || "").toUpperCase() === "HEADING" ? "var(--osi-font-heading)" : undefined,
|
|
29
|
+
fontWeight: weightOf(v.WEIGHT),
|
|
30
|
+
letterSpacing: v.LETTER_SPACING ? `${Number(v.LETTER_SPACING) / 100}em` : undefined,
|
|
31
|
+
textTransform: CASES[String(v.CASE || "").toUpperCase()],
|
|
32
|
+
color: v.COLOR || undefined,
|
|
33
|
+
textDecoration: v.URL && v.UNDERLINE ? "underline" : undefined,
|
|
34
|
+
}).filter(([, x]) => x !== undefined));
|
|
35
|
+
return {
|
|
36
|
+
id: b.key,
|
|
37
|
+
key: b.key,
|
|
38
|
+
text: t(v.TEXT),
|
|
39
|
+
url: v.URL || null,
|
|
40
|
+
newTab: !!v.OPEN_IN_NEW_TAB,
|
|
41
|
+
background: v.BACKGROUND || null,
|
|
42
|
+
style,
|
|
43
|
+
};
|
|
44
|
+
})
|
|
45
|
+
.filter((m) => m.text)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Only without blocks: the store's announcements.
|
|
49
|
+
const useBlocks = computed(() => blockMessages.value.length > 0);
|
|
50
|
+
const { announcements, loadAnnouncements } = useAnnouncements();
|
|
51
|
+
if (!useBlocks.value) (([__temp, __restore] = withAsyncContext(() => loadAnnouncements())), (__temp = await __temp), __restore(), __temp);
|
|
52
|
+
|
|
53
|
+
const apiMessages = computed(() => {
|
|
54
|
+
const raw = announcements.value;
|
|
55
|
+
const list = Array.isArray(raw) ? raw : raw?.results ?? [];
|
|
56
|
+
return list
|
|
57
|
+
.map((entry) => ({
|
|
58
|
+
id: entry.id,
|
|
59
|
+
key: null,
|
|
60
|
+
text: t(entry.text ?? entry.message ?? entry.title),
|
|
61
|
+
url: entry.url || entry.link || null,
|
|
62
|
+
newTab: false,
|
|
63
|
+
background: null,
|
|
64
|
+
style: {},
|
|
65
|
+
}))
|
|
66
|
+
.filter((entry) => entry.text);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const messages = computed(() => (useBlocks.value ? blockMessages.value : apiMessages.value));
|
|
70
|
+
|
|
71
|
+
const visible = computed(
|
|
72
|
+
() => settings.value.ANNOUNCEMENT_ENABLED !== false && messages.value.length > 0
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const { slides, onSwiper, onSlideChange, activeIndex, setNavigationEnabled, goPrev, goNext } = useCarousel({
|
|
76
|
+
items: () => messages.value,
|
|
77
|
+
loop: () => messages.value.length > 1,
|
|
78
|
+
minLoopSlides: 3,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const fade = computed(() => String(settings.value.ANNOUNCEMENT_TRANSITION || "SLIDE").toUpperCase() === "FADE");
|
|
82
|
+
const modules = [Autoplay, Navigation, EffectFade];
|
|
83
|
+
|
|
84
|
+
const autoplay = computed(() =>
|
|
85
|
+
settings.value.ANNOUNCEMENT_AUTOPLAY !== false && messages.value.length > 1
|
|
86
|
+
? { delay: (settings.value.ANNOUNCEMENT_SPEED || 5) * 1000, disableOnInteraction: false }
|
|
87
|
+
: false
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Arrows on phones (ANNOUNCEMENT_SHOW_ARROWS_MOBILE) and desktop
|
|
91
|
+
// (ANNOUNCEMENT_SHOW_ARROWS), each switchable. Swiper needs
|
|
92
|
+
// navigation.init()/destroy(), not just the param flipped, which is what
|
|
93
|
+
// setNavigationEnabled does.
|
|
94
|
+
const showArrows = computed(
|
|
95
|
+
() =>
|
|
96
|
+
messages.value.length > 1 &&
|
|
97
|
+
(isMobile.value
|
|
98
|
+
? settings.value.ANNOUNCEMENT_SHOW_ARROWS_MOBILE !== false
|
|
99
|
+
: settings.value.ANNOUNCEMENT_SHOW_ARROWS !== false)
|
|
100
|
+
);
|
|
101
|
+
watch(showArrows, (enabled) => setNavigationEnabled(enabled), { immediate: true });
|
|
102
|
+
|
|
103
|
+
/** The bar: its scheme, the showing message's own background, padding, width, line. */
|
|
104
|
+
const barStyle = computed(() => {
|
|
105
|
+
const current = slides.value[activeIndex.value] ?? messages.value[0];
|
|
106
|
+
const s = settings.value;
|
|
107
|
+
return {
|
|
108
|
+
...scheme.value.style,
|
|
109
|
+
background: current?.background || "var(--osi-bg)",
|
|
110
|
+
color: "var(--osi-text)",
|
|
111
|
+
"--ann-py": px(s.ANNOUNCEMENT_PADDING_Y) ?? "15px",
|
|
112
|
+
"--ann-max": String(s.ANNOUNCEMENT_WIDTH || "PAGE").toUpperCase() === "FULL" ? "none" : "var(--osi-page-width)",
|
|
113
|
+
borderBottom: s.ANNOUNCEMENT_DIVIDER ? "1px solid var(--osi-border)" : undefined,
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return { settings, scheme, isMobile, t, px, WEIGHTS, weightOf, CASES, blockMessages, useBlocks, announcements, loadAnnouncements, apiMessages, messages, visible, slides, onSwiper, onSlideChange, activeIndex, setNavigationEnabled, goPrev, goNext, fade, modules, autoplay, showArrows, barStyle };
|
|
118
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { NuxtLink } from "#components";
|
|
2
|
+
import { buttonClasses as toButtonClasses, buttonVars } from "#osi/composables/useButtonStyle";
|
|
3
|
+
|
|
4
|
+
export default function useLogic({ props }) {
|
|
5
|
+
const { s, t, shellStyle, innerStyle, align, customClass, heading } = useSectionShell(() => props.section);
|
|
6
|
+
const { mediaUrl } = useMediaUrl();
|
|
7
|
+
const { isMobile } = useLayoutShell();
|
|
8
|
+
const placeholders = usePlaceholders();
|
|
9
|
+
const NuxtLinkComponent = NuxtLink;
|
|
10
|
+
|
|
11
|
+
const isPromo = computed(() => props.variant === "promo");
|
|
12
|
+
const has = (v) => v !== undefined && v !== null && v !== "";
|
|
13
|
+
const num = (v, d) => (has(v) && Number.isFinite(Number(v)) ? Number(v) : d);
|
|
14
|
+
const px = (v) => (has(v) && Number.isFinite(Number(v)) ? `${Number(v)}px` : undefined);
|
|
15
|
+
|
|
16
|
+
const VIDEO_EXT = /\.(mp4|webm|mov|m4v)(\?.*)?$/i;
|
|
17
|
+
const isVideoUrl = (url) => !!url && VIDEO_EXT.test(url);
|
|
18
|
+
|
|
19
|
+
const RATIOS = { SQUARE: "1 / 1", PORTRAIT: "3 / 4", PORTRAIT_4_5: "4 / 5", LANDSCAPE: "4 / 3", WIDE: "16 / 9", NATURAL: "auto" };
|
|
20
|
+
const SPOTS = {
|
|
21
|
+
TOP_LEFT: ["flex-start", "flex-start"], TOP_CENTER: ["flex-start", "center"], TOP_RIGHT: ["flex-start", "flex-end"],
|
|
22
|
+
CENTER_LEFT: ["center", "flex-start"], CENTER: ["center", "center"], CENTER_RIGHT: ["center", "flex-end"],
|
|
23
|
+
BOTTOM_LEFT: ["flex-end", "flex-start"], BOTTOM_CENTER: ["flex-end", "center"], BOTTOM_RIGHT: ["flex-end", "flex-end"],
|
|
24
|
+
};
|
|
25
|
+
const TEXT_ALIGN = { LEFT: "start", CENTER: "center", RIGHT: "end" };
|
|
26
|
+
const ITEMS = { LEFT: "flex-start", CENTER: "center", RIGHT: "flex-end" };
|
|
27
|
+
|
|
28
|
+
const defaultPosition = computed(() => (isPromo.value ? "OVERLAY" : "BELOW"));
|
|
29
|
+
|
|
30
|
+
const cards = computed(() => {
|
|
31
|
+
const rows = (props.section.blocks ?? [])
|
|
32
|
+
.filter((b) => !b.disabled)
|
|
33
|
+
.map((b) => {
|
|
34
|
+
const bs = b.settings ?? {};
|
|
35
|
+
const media = isMobile.value ? bs.MOBILE_MEDIA || bs.MEDIA : bs.MEDIA;
|
|
36
|
+
const src = mediaUrl(media ?? b.media ?? null);
|
|
37
|
+
const position = String(bs.CARD_TEXT_POSITION || s.value.TEXT_POSITION || defaultPosition.value).toUpperCase();
|
|
38
|
+
const buttonLabel = t(bs.BUTTON_LABEL);
|
|
39
|
+
return {
|
|
40
|
+
key: b.id ?? b.key,
|
|
41
|
+
src,
|
|
42
|
+
isVideo: isVideoUrl(src),
|
|
43
|
+
poster: mediaUrl(bs.POSTER ?? null) || null,
|
|
44
|
+
alt: t(bs.ALT) || t(bs.HEADING) || "",
|
|
45
|
+
eyebrow: t(bs.EYEBROW),
|
|
46
|
+
heading: t(bs.HEADING),
|
|
47
|
+
text: t(bs.TEXT),
|
|
48
|
+
url: bs.URL || null,
|
|
49
|
+
button: buttonLabel && bs.URL
|
|
50
|
+
? { label: buttonLabel, style: buttonVars(bs, "BUTTON"), classes: toButtonClasses(bs, "BUTTON", ["osi-ig-btn"]) }
|
|
51
|
+
: null,
|
|
52
|
+
overlay: position === "OVERLAY",
|
|
53
|
+
style: {
|
|
54
|
+
"--ig-span-c": num(bs.SPAN_COLUMNS, 1),
|
|
55
|
+
"--ig-span-r": num(bs.SPAN_ROWS, 1),
|
|
56
|
+
...(has(bs.OVERLAY_OPACITY) ? { "--ig-ov-opacity": String(Number(bs.OVERLAY_OPACITY) / 100) } : {}),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!rows.length && placeholders.allowed?.value) {
|
|
62
|
+
return Array.from({ length: isPromo.value ? 2 : 3 }, (_, i) => ({
|
|
63
|
+
key: `ph-${i}`,
|
|
64
|
+
src: null,
|
|
65
|
+
isVideo: false,
|
|
66
|
+
poster: null,
|
|
67
|
+
alt: "",
|
|
68
|
+
eyebrow: "",
|
|
69
|
+
heading: isPromo.value ? "Promotion title" : "Card title",
|
|
70
|
+
text: "",
|
|
71
|
+
url: null,
|
|
72
|
+
button: null,
|
|
73
|
+
overlay: defaultPosition.value === "OVERLAY",
|
|
74
|
+
style: {},
|
|
75
|
+
placeholder: true,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
return rows;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const subheading = computed(() => t(s.value.SUBHEADING));
|
|
82
|
+
|
|
83
|
+
const vars = computed(() => {
|
|
84
|
+
const v = s.value;
|
|
85
|
+
const spot = SPOTS[String(v.OVERLAY_POSITION || (isPromo.value ? "BOTTOM_LEFT" : "BOTTOM_LEFT")).toUpperCase()] ?? SPOTS.BOTTOM_LEFT;
|
|
86
|
+
const alignKey = String(v.TEXT_ALIGNMENT || "LEFT").toUpperCase();
|
|
87
|
+
const out = {
|
|
88
|
+
"--ig-cols": num(v.COLUMNS, isPromo.value ? 2 : 3),
|
|
89
|
+
"--ig-cols-tablet": num(v.COLUMNS_TABLET, 2),
|
|
90
|
+
"--ig-cols-mobile": num(v.COLUMNS_MOBILE, 1),
|
|
91
|
+
"--ig-gap": px(v.GAP) ?? "16px",
|
|
92
|
+
"--ig-gap-mobile": px(v.GAP_MOBILE) ?? px(v.GAP) ?? "12px",
|
|
93
|
+
"--ig-ratio": RATIOS[v.RATIO] ?? (isPromo.value ? RATIOS.LANDSCAPE : RATIOS.PORTRAIT_4_5),
|
|
94
|
+
"--ig-ratio-mobile": RATIOS[v.RATIO_MOBILE] ?? RATIOS[v.RATIO] ?? (isPromo.value ? RATIOS.LANDSCAPE : RATIOS.PORTRAIT_4_5),
|
|
95
|
+
"--ig-fit": String(v.IMAGE_FIT || "COVER").toLowerCase(),
|
|
96
|
+
"--ig-radius": px(v.CARD_RADIUS) ?? "0px",
|
|
97
|
+
"--ig-ov-y": spot[0],
|
|
98
|
+
"--ig-ov-x": spot[1],
|
|
99
|
+
"--ig-text-align": TEXT_ALIGN[alignKey] ?? "start",
|
|
100
|
+
"--ig-text-items": ITEMS[alignKey] ?? "flex-start",
|
|
101
|
+
};
|
|
102
|
+
if (v.OVERLAY_COLOR) out["--ig-ov-color"] = v.OVERLAY_COLOR;
|
|
103
|
+
if (has(v.OVERLAY_OPACITY)) out["--ig-ov-opacity-base"] = String(Number(v.OVERLAY_OPACITY) / 100);
|
|
104
|
+
if (v.OVERLAY_TEXT_COLOR) out["--ig-ov-text"] = v.OVERLAY_TEXT_COLOR;
|
|
105
|
+
if (v.CARD_BG_COLOR) out["--ig-card-bg"] = v.CARD_BG_COLOR;
|
|
106
|
+
if (px(v.CARD_PADDING)) out["--ig-card-pad"] = px(v.CARD_PADDING);
|
|
107
|
+
if (px(v.HEADING_SIZE)) out["--ig-h-fs"] = px(v.HEADING_SIZE);
|
|
108
|
+
if (px(v.HEADING_SIZE_MOBILE)) out["--ig-h-fs-mobile"] = px(v.HEADING_SIZE_MOBILE);
|
|
109
|
+
if (v.HEADING_COLOR) out["--ig-h-color"] = v.HEADING_COLOR;
|
|
110
|
+
if (px(v.TEXT_SIZE)) out["--ig-t-fs"] = px(v.TEXT_SIZE);
|
|
111
|
+
return out;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const rootClasses = computed(() => [
|
|
115
|
+
`osi-ig--${props.variant}`,
|
|
116
|
+
`osi-ig--hover-${String(s.value.HOVER_EFFECT || "ZOOM").toLowerCase()}`,
|
|
117
|
+
{ "osi-ig--natural": s.value.RATIO === "NATURAL" },
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
function cardTag(card) {
|
|
121
|
+
return card.url && !card.button && !card.placeholder ? NuxtLinkComponent : "div";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { s, t, shellStyle, innerStyle, align, customClass, heading, mediaUrl, isMobile, placeholders, NuxtLinkComponent, isPromo, has, num, px, VIDEO_EXT, isVideoUrl, RATIOS, SPOTS, TEXT_ALIGN, ITEMS, defaultPosition, cards, subheading, vars, rootClasses, cardTag };
|
|
125
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
|
|
2
|
+
export default function useLogic({ props }) {
|
|
3
|
+
const s = computed(() => props.settings ?? {});
|
|
4
|
+
const layout = computed(() => String(s.value.LAYOUT ?? "GRID").toUpperCase());
|
|
5
|
+
const isCarousel = computed(() => layout.value === "CAROUSEL");
|
|
6
|
+
const config = computed(() => ({ template: props.template }));
|
|
7
|
+
|
|
8
|
+
/* ---------------------------------------------------------------- variables */
|
|
9
|
+
|
|
10
|
+
const styleVars = computed(() => {
|
|
11
|
+
const v = s.value;
|
|
12
|
+
const num = (value, fallbackValue) => {
|
|
13
|
+
const n = Number(value);
|
|
14
|
+
return value === "" || value == null || !Number.isFinite(n) ? fallbackValue : n;
|
|
15
|
+
};
|
|
16
|
+
const one = props.featured;
|
|
17
|
+
const vars = {
|
|
18
|
+
// grid (FEATURED: one full-width card)
|
|
19
|
+
"--osi-cl-cols": String(one ? 1 : num(v.COLUMNS, 3)),
|
|
20
|
+
"--osi-cl-cols-tablet": String(one ? 1 : num(v.COLUMNS_TABLET, 2)),
|
|
21
|
+
"--osi-cl-cols-mobile": String(one ? 1 : num(v.COLUMNS_MOBILE, 2)),
|
|
22
|
+
"--osi-cl-gap-x": `${num(v.GAP_X, 24)}px`,
|
|
23
|
+
"--osi-cl-gap-y": `${num(v.GAP_Y, 32)}px`,
|
|
24
|
+
// bento / editorial
|
|
25
|
+
"--osi-cl-bento-row": `${num(v.BENTO_ROW_HEIGHT, 240)}px`,
|
|
26
|
+
"--osi-cl-bento-row-mobile": `${num(v.BENTO_ROW_HEIGHT_MOBILE, 180)}px`,
|
|
27
|
+
"--osi-cl-ed-cols": String(num(v.EDITORIAL_COLUMNS, 3)),
|
|
28
|
+
"--osi-cl-ed-cols-mobile": String(num(v.EDITORIAL_COLUMNS_MOBILE, 2)),
|
|
29
|
+
"--osi-cl-ed-offset": `${num(v.EDITORIAL_OFFSET, 64)}px`,
|
|
30
|
+
};
|
|
31
|
+
const setPx = (key, value) => {
|
|
32
|
+
if (value !== undefined && value !== null && value !== "") vars[key] = `${Number(value)}px`;
|
|
33
|
+
};
|
|
34
|
+
// carousel arrows
|
|
35
|
+
const ARROW_RADIUS = { CIRCLE: "999px", SQUARE: "0px", ROUNDED: "8px" };
|
|
36
|
+
setPx("--osi-cl-arrow-size", v.ARROW_SIZE);
|
|
37
|
+
setPx("--osi-cl-arrow-icon", v.ARROW_ICON_SIZE);
|
|
38
|
+
setPx("--osi-cl-arrow-offset", v.ARROW_OFFSET);
|
|
39
|
+
setPx("--osi-cl-arrow-border-w", v.ARROW_BORDER_WIDTH);
|
|
40
|
+
if (ARROW_RADIUS[v.ARROW_SHAPE]) vars["--osi-cl-arrow-radius"] = ARROW_RADIUS[v.ARROW_SHAPE];
|
|
41
|
+
if (v.ARROW_BG) vars["--osi-cl-arrow-bg"] = v.ARROW_BG;
|
|
42
|
+
if (v.ARROW_COLOR) vars["--osi-cl-arrow-color"] = v.ARROW_COLOR;
|
|
43
|
+
if (v.ARROW_BORDER_COLOR) vars["--osi-cl-arrow-border"] = v.ARROW_BORDER_COLOR;
|
|
44
|
+
if (v.ARROW_HOVER_BG) vars["--osi-cl-arrow-hover-bg"] = v.ARROW_HOVER_BG;
|
|
45
|
+
if (v.ARROW_HOVER_COLOR) vars["--osi-cl-arrow-hover-color"] = v.ARROW_HOVER_COLOR;
|
|
46
|
+
if (v.ARROW_SHADOW) vars["--osi-cl-arrow-shadow"] = "0 4px 14px -4px rgba(0,0,0,0.25)";
|
|
47
|
+
vars["--osi-cl-arrows-justify"] =
|
|
48
|
+
{ LEFT: "flex-start", CENTER: "center", RIGHT: "flex-end" }[v.ARROWS_BELOW_ALIGN] ?? "center";
|
|
49
|
+
// carousel dots
|
|
50
|
+
setPx("--osi-cl-dot-size", v.DOT_SIZE);
|
|
51
|
+
setPx("--osi-cl-dot-active-width", v.DOT_ACTIVE_WIDTH);
|
|
52
|
+
setPx("--osi-cl-dot-gap", v.DOT_GAP);
|
|
53
|
+
setPx("--osi-cl-dots-space", v.DOTS_SPACING);
|
|
54
|
+
if (v.DOT_COLOR) vars["--osi-cl-dot-color"] = v.DOT_COLOR;
|
|
55
|
+
if (v.DOT_ACTIVE_COLOR) vars["--osi-cl-dot-active-color"] = v.DOT_ACTIVE_COLOR;
|
|
56
|
+
vars["--osi-cl-dots-justify"] =
|
|
57
|
+
{ LEFT: "flex-start", CENTER: "center", RIGHT: "flex-end" }[v.DOTS_ALIGN] ?? "center";
|
|
58
|
+
vars["--osi-cl-dots-text-align"] =
|
|
59
|
+
{ LEFT: "start", CENTER: "center", RIGHT: "end" }[v.DOTS_ALIGN] ?? "center";
|
|
60
|
+
return vars;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const perView = computed(() => Number(s.value.SLIDES_PER_VIEW) || 4);
|
|
64
|
+
|
|
65
|
+
const carouselOptions = computed(() => ({
|
|
66
|
+
perView: perView.value,
|
|
67
|
+
perViewTablet: Number(s.value.SLIDES_PER_VIEW_TABLET) || 3,
|
|
68
|
+
perViewMobile: Number(s.value.SLIDES_PER_VIEW_MOBILE) || 1.3,
|
|
69
|
+
gap: Number(s.value.GAP_X ?? 24),
|
|
70
|
+
loop: !!s.value.CAROUSEL_LOOP,
|
|
71
|
+
autoplay: !!(s.value.CAROUSEL_AUTOPLAY ?? s.value.AUTOPLAY),
|
|
72
|
+
speed: Number(s.value.CAROUSEL_SPEED) || 5,
|
|
73
|
+
gapMobile: s.value.GAP_X_MOBILE ?? undefined,
|
|
74
|
+
showArrows: s.value.SHOW_ARROWS !== false,
|
|
75
|
+
// The old "in the header" spot is now just above the list, on the right.
|
|
76
|
+
arrowPosition: String(s.value.ARROW_POSITION || "SIDES").toUpperCase(),
|
|
77
|
+
arrowsOnHover: String(s.value.ARROW_VISIBILITY || "ALWAYS").toUpperCase() === "HOVER",
|
|
78
|
+
arrowsOnMobile: !!s.value.ARROW_SHOW_MOBILE,
|
|
79
|
+
arrowSvg: s.value.ARROW_SVG || "",
|
|
80
|
+
showDots: !!s.value.SHOW_DOTS,
|
|
81
|
+
dotsStyle: String(s.value.DOTS_STYLE || "DOTS").toUpperCase(),
|
|
82
|
+
dotsDynamic: !!s.value.DOTS_DYNAMIC,
|
|
83
|
+
railClass: s.value.RAIL_CUSTOM_CLASS || "",
|
|
84
|
+
arrowClass: s.value.ARROW_CUSTOM_CLASS || "",
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
/* Arrows "in the header": above the list, driven through the carousel. */
|
|
88
|
+
const carousel = ref(null);
|
|
89
|
+
const headerArrows = computed(
|
|
90
|
+
() =>
|
|
91
|
+
isCarousel.value &&
|
|
92
|
+
s.value.SHOW_ARROWS !== false &&
|
|
93
|
+
String(s.value.ARROW_POSITION || "SIDES").toUpperCase() === "HEADER" &&
|
|
94
|
+
props.items.length > perView.value
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return { s, layout, isCarousel, config, styleVars, perView, carouselOptions, carousel, headerArrows };
|
|
98
|
+
}
|