@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.
Files changed (206) hide show
  1. package/README.md +80 -2
  2. package/THEME_DEV_API.md +80 -0
  3. package/app/app.vue +154 -0
  4. package/app/assets/css/osi-controls.css +824 -0
  5. package/app/components/SectionRenderer.vue +46 -0
  6. package/app/components/builder/BuilderOverlay.vue +582 -0
  7. package/app/components/checkout/ExtraFees.vue +329 -0
  8. package/app/components/checkout/PaymentHandoff.vue +388 -0
  9. package/app/components/common/Banner.vue +35 -0
  10. package/app/components/common/ConfirmDeleteModal.vue +117 -0
  11. package/app/components/common/DropdownSelect.vue +432 -0
  12. package/app/components/common/Loader.vue +32 -0
  13. package/app/components/common/OsiField.vue +218 -0
  14. package/app/components/common/PhoneNumInput.vue +434 -0
  15. package/app/composables/UseBodyScrollLock.js +122 -0
  16. package/app/composables/UseThemeGroup.js +575 -0
  17. package/app/composables/useAddToCart.js +87 -0
  18. package/app/composables/useAnalytics.js +15 -0
  19. package/app/composables/useAnnouncements.js +29 -0
  20. package/app/composables/useApiErrorToast.js +104 -0
  21. package/app/composables/useAuth.js +99 -0
  22. package/app/composables/useBanners.js +25 -0
  23. package/app/composables/useBuilderPreview.js +320 -0
  24. package/app/composables/useButtonStyle.js +107 -0
  25. package/app/composables/useCarousel.js +160 -0
  26. package/app/composables/useCart.js +109 -0
  27. package/app/composables/useCheckout.js +76 -0
  28. package/app/composables/useClassificationItems.js +269 -0
  29. package/app/composables/useClassificationPage.js +545 -0
  30. package/app/composables/useClassificationUrl.js +63 -0
  31. package/app/composables/useColorScheme.js +44 -0
  32. package/app/composables/useContactForm.js +53 -0
  33. package/app/composables/useContentTree.js +770 -0
  34. package/app/composables/useDragScroll.js +73 -0
  35. package/app/composables/useFooterSocials.js +107 -0
  36. package/app/composables/useInputStyle.js +45 -0
  37. package/app/composables/useLayoutShell.js +114 -0
  38. package/app/composables/useLazyVideo.js +36 -0
  39. package/app/composables/useMediaTypes.js +66 -0
  40. package/app/composables/useMediaUrl.js +71 -0
  41. package/app/composables/useMenu.js +100 -0
  42. package/app/composables/useNavStyle.js +219 -0
  43. package/app/composables/usePlaceholders.js +294 -0
  44. package/app/composables/usePolicies.js +26 -0
  45. package/app/composables/useProductCardMedia.js +135 -0
  46. package/app/composables/useProductCardStyle.js +132 -0
  47. package/app/composables/useProductDetail.js +982 -0
  48. package/app/composables/useProductGallery.js +166 -0
  49. package/app/composables/useProductItems.js +270 -0
  50. package/app/composables/useProductUrl.js +60 -0
  51. package/app/composables/useProductVariants.js +210 -0
  52. package/app/composables/useQuickShop.js +115 -0
  53. package/app/composables/useRelatedProducts.js +186 -0
  54. package/app/composables/useRenderContext.js +45 -0
  55. package/app/composables/useScopedCss.js +106 -0
  56. package/app/composables/useSearch.js +35 -0
  57. package/app/composables/useSectionData.js +462 -0
  58. package/app/composables/useSectionShell.js +113 -0
  59. package/app/composables/useSidebar.js +69 -0
  60. package/app/composables/useStoreData.js +57 -0
  61. package/app/composables/useStoreInit.js +52 -0
  62. package/app/composables/useStorefront.js +239 -0
  63. package/app/composables/useT.js +21 -0
  64. package/app/composables/useTextStyles.js +84 -0
  65. package/app/composables/useThemeControls.js +34 -0
  66. package/app/composables/useThemeSeo.js +91 -0
  67. package/app/composables/useThemeTokens.js +171 -0
  68. package/app/composables/useWishlist.js +66 -0
  69. package/app/logic/components/AnnouncementBar.js +118 -0
  70. package/app/logic/components/CardGrid.js +125 -0
  71. package/app/logic/components/CardList.js +98 -0
  72. package/app/logic/components/CartDrawer.js +273 -0
  73. package/app/logic/components/CartItem.js +31 -0
  74. package/app/logic/components/ClassificationBento.js +6 -0
  75. package/app/logic/components/ClassificationCard.js +6 -0
  76. package/app/logic/components/ClassificationCarousel.js +115 -0
  77. package/app/logic/components/ClassificationEditorial.js +41 -0
  78. package/app/logic/components/ClassificationFeatured.js +57 -0
  79. package/app/logic/components/ClassificationGrid.js +6 -0
  80. package/app/logic/components/ClassificationNav.js +61 -0
  81. package/app/logic/components/ClassificationPart.js +72 -0
  82. package/app/logic/components/FooterBlock.js +91 -0
  83. package/app/logic/components/FooterSocials.js +60 -0
  84. package/app/logic/components/HeaderDropdownList.js +29 -0
  85. package/app/logic/components/HeaderIcon.js +68 -0
  86. package/app/logic/components/HeaderLogo.js +39 -0
  87. package/app/logic/components/HeaderNav.js +268 -0
  88. package/app/logic/components/MobileMenu.js +280 -0
  89. package/app/logic/components/ProductBuyButtons.js +66 -0
  90. package/app/logic/components/ProductCard.js +160 -0
  91. package/app/logic/components/ProductFilters.js +37 -0
  92. package/app/logic/components/ProductGallery.js +19 -0
  93. package/app/logic/components/ProductInfoBlock.js +411 -0
  94. package/app/logic/components/ProductLightbox.js +12 -0
  95. package/app/logic/components/ProductQuantity.js +31 -0
  96. package/app/logic/components/ProductVariantPicker.js +125 -0
  97. package/app/logic/components/QuickBuy.js +194 -0
  98. package/app/logic/components/SearchOverlay.js +41 -0
  99. package/app/logic/components/SidebarMenuItem.js +39 -0
  100. package/app/logic/components/SiteFooter.js +476 -0
  101. package/app/logic/components/SiteHeader.js +266 -0
  102. package/app/logic/components/WhatsAppButton.js +75 -0
  103. package/app/logic/layouts/default.js +66 -0
  104. package/app/logic/sections/AnnouncementBarSection.js +6 -0
  105. package/app/logic/sections/BlockCarousel.js +103 -0
  106. package/app/logic/sections/ClassificationListSection.js +40 -0
  107. package/app/logic/sections/ContactFormSection.js +294 -0
  108. package/app/logic/sections/ContactInfoSection.js +419 -0
  109. package/app/logic/sections/ContentNode.js +189 -0
  110. package/app/logic/sections/CountdownSection.js +254 -0
  111. package/app/logic/sections/CustomHtmlSection.js +51 -0
  112. package/app/logic/sections/DividerSection.js +31 -0
  113. package/app/logic/sections/FaqSection.js +53 -0
  114. package/app/logic/sections/FeatureListSection.js +6 -0
  115. package/app/logic/sections/FeaturedProductSection.js +40 -0
  116. package/app/logic/sections/FooterSection.js +16 -0
  117. package/app/logic/sections/FooterUtilitiesSection.js +24 -0
  118. package/app/logic/sections/GallerySection.js +232 -0
  119. package/app/logic/sections/HeaderSection.js +24 -0
  120. package/app/logic/sections/HeroSection.js +24 -0
  121. package/app/logic/sections/LogoListSection.js +6 -0
  122. package/app/logic/sections/MainClassificationSection.js +473 -0
  123. package/app/logic/sections/MainProductSection.js +211 -0
  124. package/app/logic/sections/MainSearchSection.js +6 -0
  125. package/app/logic/sections/MapSection.js +92 -0
  126. package/app/logic/sections/MediaContentSection.js +38 -0
  127. package/app/logic/sections/MediaWithTextSection.js +238 -0
  128. package/app/logic/sections/NewsletterSection.js +126 -0
  129. package/app/logic/sections/ProductHighlightSection.js +201 -0
  130. package/app/logic/sections/ProductListSection.js +45 -0
  131. package/app/logic/sections/ProductRecommendationsSection.js +113 -0
  132. package/app/logic/sections/RichTextSection.js +6 -0
  133. package/app/logic/sections/ScrollingTextSection.js +69 -0
  134. package/app/logic/sections/SearchHeaderSection.js +22 -0
  135. package/app/logic/sections/SlideshowSection.js +118 -0
  136. package/app/logic/sections/SpacerSection.js +37 -0
  137. package/app/logic/sections/SplitShowcaseSection.js +6 -0
  138. package/app/logic/sections/TestimonialListSection.js +6 -0
  139. package/app/logic/sections/VideoSection.js +235 -0
  140. package/app/pages/[category]/[product].vue +196 -0
  141. package/app/pages/__builder-preview.vue +38 -0
  142. package/app/pages/categories/[slug].vue +208 -0
  143. package/app/pages/checkout.vue +1972 -0
  144. package/app/pages/forgot-password.vue +115 -0
  145. package/app/pages/index.vue +64 -0
  146. package/app/pages/login.vue +585 -0
  147. package/app/pages/order-detail.vue +442 -0
  148. package/app/pages/pages/[handle].vue +58 -0
  149. package/app/pages/privacy-policy.vue +46 -0
  150. package/app/pages/resetpassword.vue +138 -0
  151. package/app/pages/resetverificationcode.vue +163 -0
  152. package/app/pages/result.vue +203 -0
  153. package/app/pages/return-policy.vue +50 -0
  154. package/app/pages/search.vue +100 -0
  155. package/app/pages/shipping-policy.vue +46 -0
  156. package/app/pages/user-profile/components/AddAddressModal.vue +452 -0
  157. package/app/pages/user-profile/components/Whishlist.vue +137 -0
  158. package/app/pages/user-profile/components/left-bar.vue +233 -0
  159. package/app/pages/user-profile/components/orders.vue +162 -0
  160. package/app/pages/user-profile/components/profile-details.vue +566 -0
  161. package/app/pages/user-profile/components/settings.vue +194 -0
  162. package/app/pages/user-profile/index.vue +252 -0
  163. package/app/pages/verificationcode.vue +188 -0
  164. package/app/plugins/analytics.client.js +99 -0
  165. package/app/plugins/geo.ts +24 -0
  166. package/app/plugins/reveal.client.js +53 -0
  167. package/app/plugins/toast.js +10 -0
  168. package/app/plugins/vue-query.ts +5 -0
  169. package/app/services/ApiServices.js +74 -0
  170. package/app/services/AuthBus.js +162 -0
  171. package/app/services/Authentication/AuthenticationService.js +133 -0
  172. package/app/services/Authentication/TokenService.js +62 -0
  173. package/app/services/CartServices.js +91 -0
  174. package/app/services/CartToken.js +26 -0
  175. package/app/services/CategoryServices.js +54 -0
  176. package/app/services/CheckoutServices.js +113 -0
  177. package/app/services/HomeServices.js +133 -0
  178. package/app/services/NewsletterServices.js +29 -0
  179. package/app/services/PaymentServices.js +71 -0
  180. package/app/services/PoliciesServices.js +16 -0
  181. package/app/services/ProductServices.js +79 -0
  182. package/app/services/SearchServices.js +18 -0
  183. package/app/services/UserServices.js +105 -0
  184. package/app/services/UtilsServices.js +46 -0
  185. package/app/services/WishlistServices.js +51 -0
  186. package/app/services/storefront.service.js +57 -0
  187. package/app/stores/userStore.js +13 -0
  188. package/app/theme/categoryTree.js +68 -0
  189. package/app/theme/controls.js +88 -0
  190. package/app/theme/registry.js +56 -0
  191. package/app/utils/analytics.js +864 -0
  192. package/app/utils/device.js +50 -0
  193. package/app/utils/eventBus.js +24 -0
  194. package/app/utils/icon-presets.js +24 -0
  195. package/app/utils/productOffers.js +118 -0
  196. package/app/utils/requestCache.js +36 -0
  197. package/app/utils/scrollToError.js +59 -0
  198. package/app/utils/toPlainError.js +30 -0
  199. package/bin/osimart-theme.mjs +259 -0
  200. package/nuxt/osi-sections.ts +56 -0
  201. package/nuxt.config.ts +60 -0
  202. package/package.json +43 -3
  203. package/public/css/input.css +29 -0
  204. package/public/css/output.css +2716 -0
  205. package/public/favicon-32x32.png +0 -0
  206. package/public/robots.txt +2 -0
@@ -0,0 +1,266 @@
1
+ import { SECTION_WIDTHS } from "#osi/composables/useSectionShell";
2
+
3
+ export default function useLogic() {
4
+ const settings = useThemeGroup("HEADER");
5
+
6
+ // Header section blocks (HeaderSection provides them): each part carries its
7
+ // block's key for the editor, and a deleted block hides its part. Without a
8
+ // Header section (older backends) both parts always show.
9
+ const headerBlocks = inject("osiHeaderBlocks", computed(() => ({})));
10
+ const { theme: themeSettings } = useStorefront();
11
+ const showLogo = computed(() => themeSettings.value.HEADER_SHOW_LOGO !== false);
12
+ const showMenu = computed(() => themeSettings.value.HEADER_SHOW_MENU !== false);
13
+ const scheme = useColorScheme(computed(() => settings.value.HEADER_COLOR_SCHEME));
14
+
15
+ const { store, cartType } = useStorefront();
16
+ const { useMenuByHandle } = useMenu();
17
+ const { scrolled, headerPinned } = useLayoutShell();
18
+ const { toggleCartSidebar, toggleMobileSidebar, isMobileSidebarVisible } = useSidebar();
19
+ const { count } = useCart();
20
+ const { isLoggedIn } = useAuth();
21
+ const route = useRoute();
22
+ const router = useRouter();
23
+
24
+ const menu = useMenuByHandle(computed(() => settings.value.HEADER_MENU));
25
+ const pinned = headerPinned(computed(() => settings.value.HEADER_STICKY));
26
+ // Nav spacing, typography and state styling all arrive as CSS variables, so
27
+ // the markup below never names a setting. See useNavStyle.js.
28
+ const { style: navStyle, navClass, buttonStyle } = useHeaderNavStyle();
29
+ const { isActive } = useNavActive();
30
+ const { enabled: sidebarEnabled } = useSidebarStyle();
31
+
32
+ /**
33
+ * The burger opens the sidebar, so it hides when the sidebar is switched off —
34
+ * a button that opens nothing is worse than no button. HEADER_MENU_BUTTON_
35
+ * VISIBILITY then decides whether it shows on desktop too; the CSS handles
36
+ * that breakpoint, since it's a media query, not a JS one.
37
+ */
38
+ const showMenuButton = computed(
39
+ () => settings.value.HEADER_SHOW_MENU_BUTTON !== false && sidebarEnabled.value
40
+ );
41
+
42
+ /**
43
+ * Defended the same way HEADER_LAYOUT is: .toLowerCase() straight off the
44
+ * setting throws on any theme saved before HEADER_MENU_BUTTON_VISIBILITY
45
+ * existed, and it takes the whole header down with it.
46
+ */
47
+ const menuButtonVisibility = computed(() =>
48
+ String(settings.value.HEADER_MENU_BUTTON_VISIBILITY ?? "MOBILE_ONLY").toLowerCase()
49
+ );
50
+
51
+ /**
52
+ * HEADER_LAYOUT, read once and defended.
53
+ *
54
+ * `.toLowerCase()` straight off the setting threw on a theme where the key was
55
+ * missing, which is every theme created before the HEADER group existed.
56
+ */
57
+ const layout = computed(() =>
58
+ String(settings.value.HEADER_LAYOUT ?? "LOGO_CENTER").toUpperCase()
59
+ );
60
+
61
+ /**
62
+ * Where the logo and the nav render. Two independent questions, because
63
+ * LOGO_LEFT_MENU_CENTER is the layout where the answers differ.
64
+ *
65
+ * LOGO_CENTER start: nav centre: logo end: actions
66
+ * LOGO_RIGHT start: nav centre: logo (last) end: actions
67
+ * LOGO_LEFT start: — centre: logo + nav end: actions
68
+ * LOGO_LEFT_MENU_CENTER start: logo centre: nav end: actions
69
+ *
70
+ * The last row is the reason the logo moved into its own component. Sharing the
71
+ * centre cell with the nav — the previous approach — can only ever centre the
72
+ * nav optically, because the cell isn't symmetric about the page. With the logo
73
+ * in the start cell the grid stays 1fr auto 1fr and the nav is centred exactly.
74
+ *
75
+ * It also gives the mobile arrangement for nothing: the burger renders first in
76
+ * the start cell and the logo second, so on mobile they sit side by side at the
77
+ * start, which is what this layout is for.
78
+ */
79
+ const logoInStart = computed(() => layout.value === "LOGO_LEFT_MENU_CENTER");
80
+ const navInCentre = computed(
81
+ () => layout.value === "LOGO_LEFT" || layout.value === "LOGO_LEFT_MENU_CENTER"
82
+ );
83
+
84
+ const iconSet = computed(() => settings.value.HEADER_ICON_SET);
85
+ const iconSize = computed(() => settings.value.HEADER_ICON_SIZE);
86
+
87
+ function handleCartClick() {
88
+ if (cartType.value === "PAGE") {
89
+ router.push("/cart");
90
+ } else {
91
+ toggleCartSidebar(true);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Transparent over the homepage hero (HEADER_TRANSPARENT_ON_INDEX).
97
+ *
98
+ * overlay the header overlaps the page top on the homepage — always,
99
+ * not just while transparent: a negative margin as tall as the
100
+ * header, so turning solid on scroll never moves the page (the
101
+ * old absolute ↔ sticky switch made it jump by the header's
102
+ * height).
103
+ * transparent at the top of the page, not hovered: no background, its own
104
+ * text / icon colour (HEADER_TRANSPARENT_TEXT_COLOR).
105
+ * hoverFill at the top, hovered (or a menu open inside it): a background
106
+ * (HEADER_TRANSPARENT_HOVER_BG, else the header's) and text
107
+ * colour (…_HOVER_TEXT, else the header's).
108
+ *
109
+ * The homepage is "/" — or, in the editor preview (/__builder-preview), the
110
+ * INDEX template, so the setting shows before it's saved.
111
+ */
112
+ const isIndex = computed(
113
+ () =>
114
+ route.path === "/" ||
115
+ (route.path.startsWith("/__builder-preview") &&
116
+ String(route.query.template || "INDEX").toUpperCase() === "INDEX")
117
+ );
118
+ const overlay = computed(() => !!settings.value.HEADER_TRANSPARENT_ON_INDEX && isIndex.value);
119
+ const hovered = ref(false);
120
+ const hoverFills = computed(() => settings.value.HEADER_TRANSPARENT_HOVER !== false);
121
+ const atTop = computed(() => overlay.value && !scrolled.value);
122
+ const transparent = computed(() => atTop.value && !(hovered.value && hoverFills.value));
123
+ const hoverFill = computed(() => atTop.value && hovered.value && hoverFills.value);
124
+
125
+ // The transparent header's own logo (e.g. white); the main one on hover /
126
+ // once scrolled — HeaderLogo cross-fades between them.
127
+ const { mediaUrl: logoUrl } = useMediaUrl();
128
+ const transparentLogo = computed(() =>
129
+ overlay.value ? logoUrl(settings.value.HEADER_TRANSPARENT_LOGO) || "" : ""
130
+ );
131
+
132
+ /**
133
+ * Scheme first, overrides on top. Only non-empty overrides are written, so an
134
+ * unset one leaves the scheme's variable untouched rather than blanking it.
135
+ */
136
+ const headerStyle = computed(() => {
137
+ const style = { ...scheme.value.style };
138
+ const s = settings.value;
139
+
140
+ if (s.HEADER_BACKGROUND) style["--osi-bg"] = s.HEADER_BACKGROUND;
141
+ if (s.HEADER_TEXT_COLOR) style["--osi-text"] = s.HEADER_TEXT_COLOR;
142
+ // Icons default to the text colour; the override only splits them apart when
143
+ // the merchant actually wants that.
144
+ style["--osi-header-icon"] = s.HEADER_ICON_COLOR || "var(--osi-text)";
145
+ if (s.HEADER_ICON_BG) style["--osi-header-icon-bg"] = s.HEADER_ICON_BG;
146
+ if (s.HEADER_ICON_BORDER) style["--osi-header-icon-border"] = s.HEADER_ICON_BORDER;
147
+ style["--osi-header-pad-y"] = `${s.HEADER_PADDING_Y}px`;
148
+ // Side padding lives on the header itself, not on the inner box — see the
149
+ // note on .osi-header-inner below. 16 is COMMON_STYLE_SETTINGS' PADDING_X
150
+ // default, so an unset header lines up with an unset section.
151
+ style["--osi-header-pad-x"] = `${s.HEADER_PADDING_X ?? 16}px`;
152
+ style["--osi-header-max-w"] =
153
+ SECTION_WIDTHS[s.HEADER_CONTAINER] ?? SECTION_WIDTHS.NORMAL;
154
+
155
+ if (overlay.value) style["--osi-header-overlap"] = `${headerHeight.value}px`;
156
+ if (transparent.value) {
157
+ const fg = s.HEADER_TRANSPARENT_TEXT_COLOR;
158
+ if (fg) {
159
+ style["--osi-text"] = fg;
160
+ style["--osi-header-icon"] = fg;
161
+ }
162
+ } else if (hoverFill.value) {
163
+ if (s.HEADER_TRANSPARENT_HOVER_BG) style["--osi-bg"] = s.HEADER_TRANSPARENT_HOVER_BG;
164
+ const fg = s.HEADER_TRANSPARENT_HOVER_TEXT;
165
+ if (fg) {
166
+ style["--osi-text"] = fg;
167
+ style["--osi-header-icon"] = fg;
168
+ }
169
+ }
170
+ return style;
171
+ });
172
+
173
+ /** Menu links follow the transparent / hover text colour too. */
174
+ const headerNavStyle = computed(() => {
175
+ const s = settings.value;
176
+ const fg = transparent.value
177
+ ? s.HEADER_TRANSPARENT_TEXT_COLOR
178
+ : hoverFill.value
179
+ ? s.HEADER_TRANSPARENT_HOVER_TEXT
180
+ : null;
181
+ return fg ? { ...navStyle.value, "--osi-nav-color": fg } : navStyle.value;
182
+ });
183
+
184
+ const searchStyle = computed(() =>
185
+ showSearchAllowed.value ? settings.value.HEADER_SEARCH_STYLE : "NONE"
186
+ );
187
+ const showSearchAllowed = computed(() => settings.value.HEADER_SHOW_SEARCH);
188
+
189
+ const cartCountStyle = computed(() => settings.value.HEADER_CART_COUNT);
190
+
191
+ // --- search ---------------------------------------------------------------
192
+ const searchOpen = ref(false);
193
+ const inlineQuery = ref("");
194
+
195
+ function submitInline() {
196
+ const value = inlineQuery.value.trim();
197
+ if (!value) return;
198
+ inlineQuery.value = "";
199
+ router.push(`/search?q=${encodeURIComponent(value)}`);
200
+ }
201
+
202
+ /**
203
+ * How much of the screen the header covers right now, published on <html> as
204
+ * --osi-header-offset (and its full height as --osi-header-height).
205
+ *
206
+ * Anything else that sticks to the top — the listing page's toolbar, a
207
+ * sidebar — sits at `top: var(--osi-header-offset)`. While the header is
208
+ * pinned that's its height, so they stick just under it; when it slides away
209
+ * on scroll-down it drops to 0 and they move up into the freed space. Both
210
+ * transition over the same 0.3s as the header's own slide, so the toolbar
211
+ * glides with it instead of jumping or getting covered.
212
+ *
213
+ * 0 when the header doesn't stick at all (NONE) or is floating transparent
214
+ * over the hero (absolute, so it covers nothing once scrolled past).
215
+ */
216
+ const headerEl = ref(null);
217
+ const headerHeight = ref(0);
218
+ let resizeObserver = null;
219
+
220
+ // Overlaying the hero no longer changes how the header sticks (see overlay).
221
+ const sticks = computed(
222
+ () => String(settings.value.HEADER_STICKY ?? "ON_SCROLL_UP").toUpperCase() !== "NONE"
223
+ );
224
+
225
+ function publishOffset() {
226
+ if (!import.meta.client) return;
227
+ const root = document.documentElement.style;
228
+ const offset = sticks.value && pinned.value ? headerHeight.value : 0;
229
+ root.setProperty("--osi-header-height", `${headerHeight.value}px`);
230
+ root.setProperty("--osi-header-offset", `${offset}px`);
231
+ }
232
+
233
+ onMounted(() => {
234
+ const el = headerEl.value;
235
+ if (!el) return;
236
+ headerHeight.value = el.offsetHeight;
237
+ if (typeof ResizeObserver !== "undefined") {
238
+ resizeObserver = new ResizeObserver(() => {
239
+ headerHeight.value = el.offsetHeight;
240
+ });
241
+ resizeObserver.observe(el);
242
+ }
243
+ publishOffset();
244
+ });
245
+
246
+ watch([sticks, pinned, headerHeight], publishOffset);
247
+
248
+ onBeforeUnmount(() => {
249
+ resizeObserver?.disconnect();
250
+ if (import.meta.client) {
251
+ document.documentElement.style.setProperty("--osi-header-offset", "0px");
252
+ }
253
+ });
254
+
255
+ // Navigation is the only reliable "done here" signal — a panel opened by click
256
+ // outlives the click that opened it. HeaderNav closes its own dropdown, since
257
+ // that state now lives with the component that owns it.
258
+ watch(
259
+ () => route.fullPath,
260
+ () => {
261
+ searchOpen.value = false;
262
+ }
263
+ );
264
+
265
+ return { settings, headerBlocks, themeSettings, showLogo, showMenu, scheme, store, cartType, useMenuByHandle, scrolled, headerPinned, toggleCartSidebar, toggleMobileSidebar, isMobileSidebarVisible, count, isLoggedIn, route, router, menu, pinned, navStyle, navClass, buttonStyle, isActive, sidebarEnabled, showMenuButton, menuButtonVisibility, layout, logoInStart, navInCentre, iconSet, iconSize, handleCartClick, isIndex, overlay, hovered, hoverFills, atTop, transparent, hoverFill, logoUrl, transparentLogo, headerStyle, headerNavStyle, searchStyle, showSearchAllowed, cartCountStyle, searchOpen, inlineQuery, submitInline, headerEl, headerHeight, resizeObserver, sticks, publishOffset };
266
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The floating WhatsApp button — theme settings → WhatsApp button (WHATSAPP_*).
3
+ *
4
+ * A bottom corner of every page (the layout renders it). Opens a chat with
5
+ * the store's number (wa.me), with an optional pre-filled message; without a
6
+ * number it falls back to the WhatsApp link in Social media. No link at all:
7
+ * nothing is drawn.
8
+ *
9
+ * The icon is the WhatsApp logo, the merchant's own image, or their SVG; on a
10
+ * coloured background (circle / rounded / square) or bare. An optional label
11
+ * sits beside it — on hover, or always on larger screens.
12
+ */
13
+
14
+
15
+ export default function useLogic({ props }) {
16
+ const s = useThemeGroup("WHATSAPP");
17
+ const social = useThemeGroup("SOCIAL_MEDIA");
18
+ const t = useT();
19
+ const { mediaUrl } = useMediaUrl();
20
+
21
+ const LOGO =
22
+ "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 0 1-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 0 1-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 0 1 2.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0 0 12.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 0 0 5.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 0 0-3.48-8.413Z";
23
+
24
+ const up = (v, d) => String(v || d).toUpperCase();
25
+
26
+ /** wa.me with the number (digits only), else the Social media link. */
27
+ const href = computed(() => {
28
+ const message = t(s.value.WHATSAPP_MESSAGE) || "";
29
+ const digits = String(s.value.WHATSAPP_NUMBER || "").replace(/\D/g, "");
30
+ if (digits) {
31
+ return `https://wa.me/${digits}${message ? `?text=${encodeURIComponent(message)}` : ""}`;
32
+ }
33
+ const url = String(social.value.SOCIAL_WHATSAPP_URL || "").trim();
34
+ if (!url) return "";
35
+ if (!message || /[?&]text=/.test(url)) return url;
36
+ return `${url}${url.includes("?") ? "&" : "?"}text=${encodeURIComponent(message)}`;
37
+ });
38
+
39
+ const visible = computed(
40
+ () => !!s.value.WHATSAPP_ENABLED && !!href.value && !(props.checkout && s.value.WHATSAPP_HIDE_ON_CHECKOUT !== false)
41
+ );
42
+
43
+ const icon = computed(() => up(s.value.WHATSAPP_ICON, "DEFAULT"));
44
+ const image = computed(() => (icon.value === "IMAGE" ? mediaUrl(s.value.WHATSAPP_IMAGE ?? null) : ""));
45
+ const svg = computed(() => (icon.value === "SVG" ? String(s.value.WHATSAPP_SVG || "").trim() : ""));
46
+ // Nothing picked for the image / SVG: the logo.
47
+ const showLogo = computed(() => !image.value && !svg.value);
48
+ const label = computed(() => t(s.value.WHATSAPP_LABEL) || "");
49
+
50
+ const px = (v, d) => `${Number.isFinite(Number(v)) && v !== "" && v != null ? Number(v) : d}px`;
51
+
52
+ const classes = computed(() => [
53
+ `at-${up(s.value.WHATSAPP_POSITION, "BOTTOM_RIGHT") === "BOTTOM_LEFT" ? "left" : "right"}`,
54
+ `shape-${up(s.value.WHATSAPP_SHAPE, "CIRCLE").toLowerCase()}`,
55
+ `show-${up(s.value.WHATSAPP_SHOW_ON, "ALL").toLowerCase()}`,
56
+ `label-${up(s.value.WHATSAPP_LABEL_SHOW, "HOVER").toLowerCase()}`,
57
+ {
58
+ "is-bare": up(s.value.WHATSAPP_STYLE, "FILLED") === "BARE",
59
+ "has-shadow": s.value.WHATSAPP_SHADOW !== false,
60
+ "is-pulsing": !!s.value.WHATSAPP_PULSE,
61
+ },
62
+ ]);
63
+
64
+ const style = computed(() => ({
65
+ "--wa-size": px(s.value.WHATSAPP_SIZE, 56),
66
+ "--wa-icon": px(s.value.WHATSAPP_ICON_SIZE, 30),
67
+ "--wa-bg": s.value.WHATSAPP_BG || "#25D366",
68
+ "--wa-color": s.value.WHATSAPP_ICON_COLOR || "#ffffff",
69
+ "--wa-x": px(s.value.WHATSAPP_OFFSET_X, 20),
70
+ "--wa-y": px(s.value.WHATSAPP_OFFSET_Y, 20),
71
+ "--wa-y-m": px(s.value.WHATSAPP_OFFSET_Y_MOBILE, 16),
72
+ }));
73
+
74
+ return { s, social, t, mediaUrl, LOGO, up, href, visible, icon, image, svg, showLogo, label, px, classes, style };
75
+ }
@@ -0,0 +1,66 @@
1
+ import { eventBus } from "#osi/utils/eventBus";
2
+ import { resolveSection } from "#osi/theme/registry";
3
+
4
+ export default function useLogic() {
5
+ /**
6
+ * One layout for the whole storefront.
7
+ *
8
+ * Terazar's `default` and `product` layouts had byte-identical scripts and only
9
+ * differed in which chrome they rendered, so this collapses to one file and a
10
+ * page-meta flag:
11
+ *
12
+ * definePageMeta({ chrome: 'minimal' }) // checkout — logo only, no nav
13
+ * definePageMeta({ chrome: 'bare' }) // no header or footer at all
14
+ *
15
+ * The `add-to-cart` emit chain is gone. Pages and sections call useAddToCart()
16
+ * directly, which fetches the cart and opens the drawer itself — a section
17
+ * rendered through a dynamic <component> can't emit up to a layout anyway.
18
+ */
19
+ const route = useRoute();
20
+ const { closeAll } = useSidebar();
21
+ const { fetchCart } = useCart();
22
+
23
+ const chrome = computed(() => route.meta.chrome ?? "full");
24
+ const showHeader = computed(() => chrome.value !== "bare");
25
+ const showFooter = computed(() => chrome.value === "full");
26
+ const showNav = computed(() => chrome.value === "full");
27
+
28
+ /**
29
+ * Header / footer, Shopify-style: the theme's HEADER_GROUP and FOOTER_GROUP
30
+ * sections, rendered around the page. Straight in the layout, no wrapper —
31
+ * the header's position: sticky needs the page as its container.
32
+ *
33
+ * full every group section
34
+ * minimal the header section only (checkout: logo, no menu)
35
+ * bare nothing
36
+ *
37
+ * A backend without groups (sectionGroups null) keeps the old fixed chrome.
38
+ */
39
+ const { sectionGroups } = useStorefront();
40
+ const { isPreview } = useBuilderPreview();
41
+ const headerSections = computed(() => {
42
+ const list = (sectionGroups.value?.header ?? []).filter((s) => isPreview.value || !s.disabled);
43
+ if (chrome.value === "bare") return [];
44
+ return chrome.value === "minimal" ? list.filter((s) => s.type === "HEADER") : list;
45
+ });
46
+ const footerSections = computed(() =>
47
+ chrome.value === "full"
48
+ ? (sectionGroups.value?.footer ?? []).filter((s) => isPreview.value || !s.disabled)
49
+ : []
50
+ );
51
+
52
+ // Drawers are useState-backed so they survive the page component swapping out.
53
+ // That's deliberate, and it means navigation has to close them explicitly.
54
+ watch(() => route.fullPath, () => closeAll());
55
+
56
+ onMounted(() => {
57
+ fetchCart();
58
+ eventBus.on("cartUpdated", fetchCart);
59
+ });
60
+
61
+ onBeforeUnmount(() => {
62
+ eventBus.off("cartUpdated", fetchCart);
63
+ });
64
+
65
+ return { route, closeAll, fetchCart, chrome, showHeader, showFooter, showNav, sectionGroups, isPreview, headerSections, footerSections, resolveSection };
66
+ }
@@ -0,0 +1,6 @@
1
+
2
+ export default function useLogic() {
3
+
4
+
5
+ return { };
6
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The swipe behaviour of a horizontal Group (or section) with CAROUSEL on:
3
+ * native scroll-snap — touch, trackpad and keyboard scrolling for free, no
4
+ * library — plus optional arrows, dots, autoplay and loop.
5
+ *
6
+ * It renders the flex track itself (trackClass / trackStyle come from the
7
+ * group's own layout), so the blocks stay direct children of a flex container
8
+ * and every width / alignment rule in ContentNode keeps applying. The slide
9
+ * sizing (per view, per view on phones) and the scroll-snap styles live in
10
+ * ContentNode too, keyed on the track's car-mobile / car-always class.
11
+ *
12
+ * Controls appear only while the track actually scrolls, so "On phones only"
13
+ * shows no arrows or dots on a desktop where the row fits. Autoplay pauses on
14
+ * hover / focus and never runs for reduced-motion visitors. RTL-safe.
15
+ *
16
+ * Controls (CAROUSEL_* on the group / section):
17
+ * arrowsAt BELOW under the slides, around the dots
18
+ * SIDES over the slides, vertically centred at each edge
19
+ * dotsAt BELOW under the slides
20
+ * INSIDE over the slides, at the bottom
21
+ * dotStyle DOT / BAR (the current one a bar) / LINE
22
+ * arrowBorder whether the arrows have a border
23
+ * controlVars sizes and colours (--bc-*), empty = the text colour
24
+ */
25
+
26
+
27
+ export default function useLogic({ props }) {
28
+ const track = ref(null);
29
+ const pages = ref(1);
30
+ const page = ref(0);
31
+ const paused = ref(false);
32
+ let timer = null;
33
+ let resize = null;
34
+
35
+ const direction = () => (track.value && getComputedStyle(track.value).direction === "rtl" ? -1 : 1);
36
+
37
+ function measure() {
38
+ const el = track.value;
39
+ if (!el) return;
40
+ const width = el.clientWidth || 1;
41
+ pages.value = el.scrollWidth > width + 1 ? Math.max(2, Math.round(el.scrollWidth / width)) : 1;
42
+ page.value = Math.min(pages.value - 1, Math.round(Math.abs(el.scrollLeft) / width));
43
+ }
44
+
45
+ function go(index) {
46
+ const el = track.value;
47
+ if (!el) return;
48
+ const target = Math.max(0, Math.min(index, pages.value - 1));
49
+ el.scrollTo({ left: direction() * target * el.clientWidth, behavior: "smooth" });
50
+ }
51
+ function next() {
52
+ if (page.value >= pages.value - 1) return props.loop && go(0);
53
+ go(page.value + 1);
54
+ }
55
+ function prev() {
56
+ if (page.value <= 0) return props.loop && go(pages.value - 1);
57
+ go(page.value - 1);
58
+ }
59
+
60
+ function start() {
61
+ stop();
62
+ if (!props.autoplay || typeof window === "undefined") return;
63
+ if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
64
+ timer = setInterval(() => {
65
+ if (!paused.value && pages.value > 1) next();
66
+ }, Math.max(1500, props.interval));
67
+ }
68
+ function stop() {
69
+ if (timer) clearInterval(timer);
70
+ timer = null;
71
+ }
72
+
73
+ onMounted(() => {
74
+ measure();
75
+ if ("ResizeObserver" in window) {
76
+ resize = new ResizeObserver(measure);
77
+ resize.observe(track.value);
78
+ }
79
+ start();
80
+ });
81
+ onBeforeUnmount(() => {
82
+ stop();
83
+ resize?.disconnect();
84
+ });
85
+ watch(() => [props.autoplay, props.interval], start);
86
+
87
+ const canPrev = computed(() => props.loop || page.value > 0);
88
+ const canNext = computed(() => props.loop || page.value < pages.value - 1);
89
+
90
+ const scrolls = computed(() => pages.value > 1);
91
+ const sideArrows = computed(() => props.arrows && props.arrowsAt === "SIDES" && scrolls.value);
92
+ const insideDots = computed(() => props.dots && props.dotsAt === "INSIDE" && scrolls.value);
93
+ const belowArrows = computed(() => props.arrows && props.arrowsAt !== "SIDES");
94
+ const belowDots = computed(() => props.dots && props.dotsAt !== "INSIDE");
95
+ const showBelow = computed(() => scrolls.value && (belowArrows.value || belowDots.value));
96
+
97
+ const rootClass = computed(() => [
98
+ `osi-bc--dots-${props.dotStyle.toLowerCase()}`,
99
+ { "osi-bc--no-arrow-border": !props.arrowBorder },
100
+ ]);
101
+
102
+ return { track, pages, page, paused, timer, resize, direction, measure, go, next, prev, start, stop, canPrev, canNext, scrolls, sideArrows, insideDots, belowArrows, belowDots, showBelow, rootClass };
103
+ }
@@ -0,0 +1,40 @@
1
+
2
+ export default function useLogic({ props }) {
3
+ const { s, shellStyle, innerStyle, customClass } = useSectionShell(() => props.section);
4
+ const { classifications, loading } = useSectionData(() => props.section);
5
+ const { items, isFeatured, sourceKind } = useClassificationItems(() => props.section, classifications);
6
+ const { tree } = useContentTree(() => props.section, {
7
+ rootDefaults: { DIRECTION: "VERTICAL", GAP: 24 },
8
+ index: () => props.index,
9
+ });
10
+
11
+ // No Card block: fall back to a plain card, so the list never disappears.
12
+ const fallback = useContentTree(() => ({
13
+ settings: {},
14
+ blocks: [
15
+ { key: "__card", type: "CARD", settings: { GAP: 10 } },
16
+ { key: "__image", type: "CARD_IMAGE", parent_key: "__card", settings: { RATIO: "SQUARE", RADIUS: 8 } },
17
+ { key: "__title", type: "CARD_TITLE", parent_key: "__card", settings: { PRESET: "H4" } },
18
+ ],
19
+ }));
20
+
21
+ const cardNode = computed(() => tree.value.children.find((n) => n.type === "CARD") ?? null);
22
+ const template = computed(() => cardNode.value ?? fallback.tree.value.children[0]);
23
+
24
+ /** The page, in block order; the list goes where the Card is (else last). */
25
+ const parts = computed(() => {
26
+ const out = tree.value.children.map((node) =>
27
+ node.type === "CARD" ? { kind: "list", key: "list" } : { kind: "block", key: node.key, node }
28
+ );
29
+ if (!cardNode.value) out.push({ kind: "list", key: "list" });
30
+ return out;
31
+ });
32
+
33
+ const visibleItems = computed(() => (isFeatured.value ? items.value.slice(0, 1) : items.value));
34
+
35
+ const emptyMessage = computed(() =>
36
+ sourceKind.value === "COLLECTION" ? "No collections to show here yet." : "No categories to show here yet."
37
+ );
38
+
39
+ return { s, shellStyle, innerStyle, customClass, classifications, loading, items, isFeatured, sourceKind, tree, fallback, cardNode, template, parts, visibleItems, emptyMessage };
40
+ }