@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,575 @@
1
+ /**
2
+ * Read a group of theme settings with defaults applied.
3
+ *
4
+ * `theme.settings` is a flat key/value map — the `group` attribute in
5
+ * THEME_SETTINGS_SCHEMA is a dashboard UI concern only, it doesn't nest the
6
+ * stored JSON. So `useThemeGroup('HEADER')` filters by prefix and layers the
7
+ * defaults below underneath.
8
+ *
9
+ * Defaults live here rather than being read from the schema because the
10
+ * storefront never receives the schema — bootstrap sends resolved values only.
11
+ * A merchant on an older theme version is missing keys the storefront already
12
+ * uses, and every one of those has to render as something.
13
+ *
14
+ * Keep these in sync with builder/schemas.py's THEME_SETTINGS_SCHEMA. If they drift,
15
+ * the storefront wins for missing keys and the dashboard wins for set ones —
16
+ * which shows up as "the default looks different from what the editor shows".
17
+ */
18
+ const DEFAULTS = {
19
+ // Shopify's "Logo and favicon"
20
+ BRAND: {
21
+ LOGO: null,
22
+ LOGO_MOBILE: null,
23
+ LOGO_WIDTH: 120,
24
+ LOGO_WIDTH_MOBILE: null,
25
+ FAVICON: null,
26
+ },
27
+
28
+ // "Search engine listing and sharing"
29
+ SEO: {
30
+ SEO_TITLE: "",
31
+ SEO_DESCRIPTION: "",
32
+ SEO_TITLE_FORMAT: "PAGE_STORE",
33
+ SEO_TITLE_SEPARATOR: "|",
34
+ SEO_SHARE_IMAGE: null,
35
+ SEO_NO_INDEX: false,
36
+ },
37
+
38
+ // "Social media" — the store-wide accounts. The footer uses its own
39
+ // FOOTER_SOCIAL_* slots first and falls back to these.
40
+ SOCIAL_MEDIA: {
41
+ SOCIAL_INSTAGRAM_URL: "",
42
+ SOCIAL_FACEBOOK_URL: "",
43
+ SOCIAL_TIKTOK_URL: "",
44
+ SOCIAL_TWITTER_URL: "",
45
+ SOCIAL_YOUTUBE_URL: "",
46
+ SOCIAL_PINTEREST_URL: "",
47
+ SOCIAL_LINKEDIN_URL: "",
48
+ SOCIAL_SNAPCHAT_URL: "",
49
+ SOCIAL_WHATSAPP_URL: "",
50
+ SOCIAL_X_HANDLE: "",
51
+ },
52
+
53
+ WHATSAPP: {
54
+ WHATSAPP_ENABLED: false,
55
+ WHATSAPP_NUMBER: "",
56
+ WHATSAPP_MESSAGE: "",
57
+ WHATSAPP_POSITION: "BOTTOM_RIGHT",
58
+ WHATSAPP_SHOW_ON: "ALL",
59
+ WHATSAPP_HIDE_ON_CHECKOUT: true,
60
+ WHATSAPP_ICON: "DEFAULT",
61
+ WHATSAPP_IMAGE: null,
62
+ WHATSAPP_SVG: "",
63
+ WHATSAPP_STYLE: "FILLED",
64
+ WHATSAPP_SHAPE: "CIRCLE",
65
+ WHATSAPP_SIZE: 56,
66
+ WHATSAPP_ICON_SIZE: 30,
67
+ WHATSAPP_BG: "#25D366",
68
+ WHATSAPP_ICON_COLOR: "#FFFFFF",
69
+ WHATSAPP_SHADOW: true,
70
+ WHATSAPP_PULSE: false,
71
+ WHATSAPP_LABEL: "",
72
+ WHATSAPP_LABEL_SHOW: "HOVER",
73
+ WHATSAPP_OFFSET_X: 20,
74
+ WHATSAPP_OFFSET_Y: 20,
75
+ WHATSAPP_OFFSET_Y_MOBILE: 16,
76
+ },
77
+
78
+ // "Animations"
79
+ ANIMATIONS: {
80
+ ANIMATION_REVEAL: false,
81
+ ANIMATION_HOVER: "NONE",
82
+ },
83
+
84
+ CUSTOM_CSS: {
85
+ CUSTOM_CSS: "",
86
+ },
87
+
88
+ ANNOUNCEMENT: {
89
+ ANNOUNCEMENT_COLOR_SCHEME: null,
90
+ ANNOUNCEMENT_AUTOPLAY: true,
91
+ ANNOUNCEMENT_SPEED: 5,
92
+ ANNOUNCEMENT_SHOW_ARROWS_MOBILE: true,
93
+ },
94
+
95
+ HEADER: {
96
+ // structure
97
+ HEADER_LAYOUT: "LOGO_CENTER",
98
+ HEADER_MENU: "main-menu",
99
+ HEADER_STICKY: "ON_SCROLL_UP",
100
+
101
+ // colour — scheme first, the three overrides fall through to it when unset
102
+ HEADER_COLOR_SCHEME: null,
103
+ HEADER_BACKGROUND: null,
104
+ HEADER_TEXT_COLOR: null,
105
+ HEADER_ICON_COLOR: null,
106
+
107
+ // icons
108
+ HEADER_ICON_SET: "LINE",
109
+ HEADER_ICON_SIZE: 20,
110
+
111
+ // search
112
+ HEADER_SHOW_SEARCH: true,
113
+ HEADER_SEARCH_STYLE: "ICON_OVERLAY",
114
+ HEADER_SEARCH_PANEL_HEIGHT: "50vh",
115
+
116
+ // cart
117
+ HEADER_CART_COUNT: "BADGE",
118
+
119
+ // chrome
120
+ HEADER_SHOW_ACCOUNT: true,
121
+ HEADER_SHOW_BORDER: true,
122
+ HEADER_TRANSPARENT_ON_INDEX: false,
123
+ HEADER_PADDING_Y: 12,
124
+
125
+ // nav links — desktop inline navigation only; the drawer is SIDEBAR
126
+ HEADER_MENU_GAP: 24,
127
+ HEADER_MENU_GAP_MOBILE: 4,
128
+ HEADER_MENU_FONT: "BODY",
129
+ HEADER_MENU_SIZE: 14,
130
+ HEADER_MENU_WEIGHT: "400",
131
+ HEADER_MENU_COLOR: null,
132
+ HEADER_MENU_HOVER_COLOR: null,
133
+ HEADER_MENU_LETTER_SPACING: 0,
134
+ HEADER_MENU_TRANSFORM: "NONE",
135
+ HEADER_MENU_HOVER_EFFECT: "UNDERLINE_GROW",
136
+ HEADER_MENU_ACTIVE_STYLE: "UNDERLINE",
137
+ HEADER_MENU_ACTIVE_COLOR: null,
138
+ HEADER_MENU_ACTIVE_BACKGROUND: null,
139
+ HEADER_MENU_ACTIVE_UNDERLINE_WIDTH: 2,
140
+ HEADER_MENU_ACTIVE_UNDERLINE_OFFSET: 6,
141
+
142
+ // mobile menu trigger — icon style/size/colour reuse HEADER_ICON_*
143
+ HEADER_SHOW_MENU_BUTTON: true,
144
+ HEADER_MENU_BUTTON_VISIBILITY: "MOBILE_ONLY",
145
+ HEADER_MENU_BUTTON_SIZE: 40,
146
+ HEADER_MENU_BUTTON_RADIUS: 0,
147
+ HEADER_MENU_BUTTON_BACKGROUND: null,
148
+ HEADER_MENU_BUTTON_HOVER_BACKGROUND: null,
149
+ },
150
+
151
+ // The sidebar renders HEADER_MENU. There is no SIDEBAR_MENU by design: two
152
+ // menus to keep in sync is one menu that silently goes stale.
153
+ SIDEBAR: {
154
+ SIDEBAR_ENABLED: true,
155
+ SIDEBAR_POSITION: "START",
156
+ SIDEBAR_WIDTH: 380,
157
+ SIDEBAR_WIDTH_MOBILE: 88,
158
+ SIDEBAR_ANIMATION: "SLIDE",
159
+ SIDEBAR_ANIMATION_DURATION: 500,
160
+ SIDEBAR_OVERLAY: true,
161
+ SIDEBAR_OVERLAY_COLOR: "#000000",
162
+ SIDEBAR_OVERLAY_OPACITY: 45,
163
+ SIDEBAR_CLOSE_ON_OVERLAY: true,
164
+ SIDEBAR_LOCK_SCROLL: true,
165
+ SIDEBAR_COLOR_SCHEME: null,
166
+ SIDEBAR_BACKGROUND: null,
167
+ SIDEBAR_TEXT_COLOR: null,
168
+ SIDEBAR_MUTED_COLOR: null,
169
+ SIDEBAR_LINK_COLOR: null,
170
+ SIDEBAR_LINK_HOVER_COLOR: null,
171
+ SIDEBAR_ACTIVE_COLOR: null,
172
+ SIDEBAR_ACTIVE_BACKGROUND: null,
173
+ SIDEBAR_BORDER_COLOR: null,
174
+ SIDEBAR_ICON_COLOR: null,
175
+ SIDEBAR_SHOW_BORDER: false,
176
+ SIDEBAR_BORDER_WIDTH: 1,
177
+ SIDEBAR_RADIUS: 0,
178
+ SIDEBAR_SHOW_DIVIDERS: false,
179
+ SIDEBAR_PADDING_TOP: 8,
180
+ SIDEBAR_PADDING_BOTTOM: 24,
181
+ SIDEBAR_PADDING_X: 20,
182
+ SIDEBAR_LINK_GAP: 2,
183
+ SIDEBAR_LINK_PADDING_Y: 12,
184
+ SIDEBAR_GROUP_GAP: 20,
185
+ SIDEBAR_LINK_FONT: "BODY",
186
+ SIDEBAR_LINK_SIZE: 16,
187
+ SIDEBAR_LINK_SIZE_MOBILE: 16,
188
+ SIDEBAR_LINK_WEIGHT: "500",
189
+ SIDEBAR_LINK_LINE_HEIGHT: 130,
190
+ SIDEBAR_LINK_LETTER_SPACING: 0,
191
+ SIDEBAR_LINK_TRANSFORM: "NONE",
192
+ SIDEBAR_SUBMENU_SIZE: 14,
193
+ SIDEBAR_SUBMENU_WEIGHT: "400",
194
+ SIDEBAR_SUBMENU_COLOR: null,
195
+ SIDEBAR_SUBMENU_MODE: "ACCORDION",
196
+ SIDEBAR_MAX_DEPTH: 3,
197
+ SIDEBAR_SHOW_ARROWS: true,
198
+ SIDEBAR_ARROW_STYLE: "CHEVRON",
199
+ SIDEBAR_ARROW_SIZE: 16,
200
+ SIDEBAR_SUBMENU_ANIMATION: "SLIDE",
201
+ SIDEBAR_SUBMENU_MULTIPLE_OPEN: false,
202
+ SIDEBAR_SUBMENU_INDENT: 16,
203
+ SIDEBAR_SHOW_HEADER: true,
204
+ SIDEBAR_HEADER_HEIGHT: 64,
205
+ SIDEBAR_HEADER_BORDER: true,
206
+ SIDEBAR_SHOW_LOGO: true,
207
+ SIDEBAR_LOGO_WIDTH: 100,
208
+ SIDEBAR_LOGO_ALIGNMENT: "LEFT",
209
+ SIDEBAR_SHOW_CLOSE: true,
210
+ SIDEBAR_CLOSE_POSITION: "END",
211
+ SIDEBAR_SHOW_FOOTER: true,
212
+ SIDEBAR_FOOTER_SHOW_ACCOUNT: true,
213
+ SIDEBAR_FOOTER_SHOW_SEARCH: false,
214
+ SIDEBAR_FOOTER_SHOW_CART: false,
215
+ SIDEBAR_FOOTER_SHOW_SOCIAL: true,
216
+ SIDEBAR_FOOTER_TEXT: null,
217
+ },
218
+
219
+ // Mirrors THEME_SETTINGS_SCHEMA's FOOTER group one-for-one. Every id, and
220
+ // every default matching the schema's — a value that differs here is a
221
+ // setting that looks one way in the editor and another on a store that
222
+ // hasn't saved it yet.
223
+ FOOTER: {
224
+ FOOTER_COLOR_SCHEME: null,
225
+ // Appearance overrides over the scheme (empty = the scheme's).
226
+ FOOTER_BACKGROUND: null,
227
+ FOOTER_TEXT_COLOR: null,
228
+ FOOTER_LINK_COLOR: null,
229
+ FOOTER_LINK_HOVER_COLOR: null,
230
+ // The bar under the footer: its own scheme / background (empty = the footer's).
231
+ FOOTER_BOTTOM_COLOR_SCHEME: null,
232
+ FOOTER_BOTTOM_BACKGROUND: null,
233
+ FOOTER_CONTAINER: "NORMAL",
234
+ FOOTER_CONTAINER_PADDING_X: 16,
235
+ FOOTER_SHOW_DIVIDER: true,
236
+ FOOTER_DIVIDER_COLOR: null,
237
+ FOOTER_PADDING_TOP: 64,
238
+ FOOTER_PADDING_BOTTOM: 32,
239
+
240
+ // logo
241
+ FOOTER_LOGO: null,
242
+ FOOTER_LOGO_POSITION: "TOP_CENTER", // HIDDEN | TOP_CENTER | INLINE_START | INLINE_END
243
+ FOOTER_LOGO_WIDTH: 64,
244
+ FOOTER_LOGO_WIDTH_MOBILE: null,
245
+ FOOTER_LOGO_ALIGNMENT: "CENTER",
246
+ FOOTER_SHOW_BRAND_DESCRIPTION: false,
247
+ FOOTER_BRAND_DESCRIPTION: "",
248
+ FOOTER_BRAND_DESCRIPTION_WIDTH: 32,
249
+
250
+ // newsletter
251
+ FOOTER_SHOW_NEWSLETTER: false,
252
+ FOOTER_NEWSLETTER_POSITION: "START", // START | END
253
+ FOOTER_NEWSLETTER_WIDTH: 32,
254
+ FOOTER_NEWSLETTER_LAYOUT: "INLINE", // INLINE | STACKED
255
+ FOOTER_NEWSLETTER_ALIGNMENT: "LEFT",
256
+ FOOTER_NEWSLETTER_HEADING: "Be a part of our world",
257
+ FOOTER_NEWSLETTER_TEXT: "",
258
+ FOOTER_NEWSLETTER_PLACEHOLDER: "Your email",
259
+ FOOTER_NEWSLETTER_BUTTON_LABEL: "Sign up",
260
+ FOOTER_NEWSLETTER_SUCCESS_MESSAGE: "Thanks for subscribing!",
261
+ FOOTER_NEWSLETTER_SHOW_CONSENT: true,
262
+ FOOTER_NEWSLETTER_CONSENT_TEXT: "",
263
+
264
+ // columns
265
+ FOOTER_COLUMNS_ALIGNMENT: "END", // START | END | SPREAD
266
+ FOOTER_COLUMNS_VERTICAL_ALIGNMENT: "TOP",
267
+ FOOTER_COLUMN_WIDTH_MODE: "EQUAL", // EQUAL | CUSTOM
268
+ FOOTER_COLUMN_GAP: 64,
269
+ FOOTER_COLUMN_ROW_GAP: 16,
270
+ FOOTER_LINK_GAP: 12,
271
+ FOOTER_COLUMN_TEXT_ALIGNMENT: "LEFT",
272
+
273
+ // No default: the schema's FOOTER_MENU_1 has none either, so a store with
274
+ // no footer menu shows no first column rather than a broken link to a
275
+ // "footer-menu" handle that may not exist.
276
+ FOOTER_MENU_1: "",
277
+ FOOTER_MENU_1_TITLE: "",
278
+ FOOTER_MENU_1_WIDTH: 33,
279
+ FOOTER_MENU_2: "",
280
+ FOOTER_MENU_2_TITLE: "",
281
+ FOOTER_MENU_2_WIDTH: 33,
282
+ FOOTER_MENU_3: "",
283
+ FOOTER_MENU_3_TITLE: "",
284
+ FOOTER_MENU_3_WIDTH: 34,
285
+
286
+ // typography — the two text_style() sets, spelled out
287
+ FOOTER_HEADING_SIZE: 13,
288
+ FOOTER_HEADING_SIZE_MOBILE: null,
289
+ FOOTER_HEADING_WEIGHT: "600",
290
+ FOOTER_HEADING_COLOR: null,
291
+ FOOTER_HEADING_LINE_HEIGHT: 130,
292
+ FOOTER_HEADING_LETTER_SPACING: 0,
293
+ FOOTER_HEADING_FONT: "BODY",
294
+ FOOTER_HEADING_TRANSFORM: "UPPERCASE",
295
+
296
+ FOOTER_LINK_SIZE: 14,
297
+ FOOTER_LINK_SIZE_MOBILE: null,
298
+ FOOTER_LINK_WEIGHT: "400",
299
+ FOOTER_LINK_COLOR: null,
300
+ FOOTER_LINK_LINE_HEIGHT: 130,
301
+ FOOTER_LINK_LETTER_SPACING: 0,
302
+ FOOTER_LINK_FONT: "BODY",
303
+ FOOTER_LINK_TRANSFORM: "NONE",
304
+
305
+ FOOTER_TEXT: "",
306
+
307
+ // social — the URL slots are the source the footer prefers; when every one
308
+ // is empty it falls back to store.social_medias
309
+ FOOTER_SHOW_SOCIAL: true,
310
+ FOOTER_SOCIAL_INSTAGRAM_URL: "",
311
+ FOOTER_SOCIAL_FACEBOOK_URL: "",
312
+ FOOTER_SOCIAL_TIKTOK_URL: "",
313
+ FOOTER_SOCIAL_TWITTER_URL: "",
314
+ FOOTER_SOCIAL_YOUTUBE_URL: "",
315
+ FOOTER_SOCIAL_PINTEREST_URL: "",
316
+ FOOTER_SOCIAL_LINKEDIN_URL: "",
317
+ FOOTER_SOCIAL_POSITION: "OWN_COLUMN", // OWN_COLUMN | UNDER_MENU | UNDER_NEWSLETTER | WITH_COPYRIGHT
318
+ FOOTER_SOCIAL_ALIGNMENT: "LEFT",
319
+ FOOTER_SOCIAL_LABEL: "Socials",
320
+ FOOTER_SOCIAL_DISPLAY: "TEXT", // TEXT | ICONS | ICON_TEXT
321
+ FOOTER_SOCIAL_ICON_STYLE: "PLAIN", // shape: PLAIN | CIRCLE | SQUARE
322
+ FOOTER_SOCIAL_ICON_BACKGROUND: "FILLED", // fill: FILLED | OUTLINE | NONE
323
+ FOOTER_SOCIAL_ICON_SIZE: 18,
324
+ FOOTER_SOCIAL_ICON_COLOR: null,
325
+ FOOTER_SOCIAL_GAP: 16,
326
+
327
+ // locale
328
+ FOOTER_SHOW_LANGUAGE_SELECTOR: false,
329
+ FOOTER_SHOW_COUNTRY_SELECTOR: false,
330
+
331
+ // bottom row
332
+ FOOTER_SHOW_PAYMENT_ICONS: false,
333
+ FOOTER_PAYMENT_ICON_SIZE: 20,
334
+ FOOTER_PAYMENT_ICON_GAP: 8,
335
+ FOOTER_COPYRIGHT: "",
336
+ FOOTER_BOTTOM_LAYOUT: "INLINE", // INLINE | THREE_ZONE
337
+ FOOTER_BOTTOM_ALIGNMENT: "CENTER", // START | CENTER | SPREAD
338
+ FOOTER_SHOW_BOTTOM_BAR_DIVIDER: true,
339
+ FOOTER_BOTTOM_PADDING_Y: 16,
340
+ FOOTER_SHOW_POWERED_BY: false,
341
+ FOOTER_POWERED_BY_TEXT: "",
342
+ FOOTER_POWERED_BY_URL: "",
343
+ FOOTER_SHOW_POLICY_LINKS: true,
344
+ FOOTER_POLICY_1_LABEL: "",
345
+ FOOTER_POLICY_1_URL: "",
346
+ FOOTER_POLICY_2_LABEL: "",
347
+ FOOTER_POLICY_2_URL: "",
348
+ FOOTER_POLICY_3_LABEL: "",
349
+ FOOTER_POLICY_3_URL: "",
350
+ FOOTER_POLICY_4_LABEL: "",
351
+ FOOTER_POLICY_4_URL: "",
352
+
353
+ // mobile
354
+ FOOTER_MOBILE_COLUMN_LAYOUT: "STACKED", // STACKED | ACCORDION
355
+ FOOTER_MOBILE_ACCORDION_EXPAND_FIRST: false,
356
+ },
357
+
358
+ // Every storefront button that isn't individually styled: cart drawer,
359
+ // login, checkout, account pages, and any section button left on defaults.
360
+ BUTTONS: {
361
+ BUTTON_STYLE: "SOLID", // SOLID | OUTLINE | UNDERLINE | TEXT
362
+ BUTTON_RADIUS: 4,
363
+ BUTTON_BORDER_WIDTH: 1,
364
+ BUTTON_SHADOW: "NONE", // NONE | SMALL | MEDIUM | LARGE (legacy: true/false)
365
+ BUTTON_SHADOW_COLOR: null,
366
+ BUTTON_PADDING_Y: 12,
367
+ BUTTON_PADDING_X: 28,
368
+ BUTTON_PADDING_Y_MOBILE: null,
369
+ BUTTON_PADDING_X_MOBILE: null,
370
+ BUTTON_FONT: "BODY",
371
+ BUTTON_FONT_SIZE: 14,
372
+ BUTTON_FONT_WEIGHT: "MEDIUM",
373
+ BUTTON_TRANSFORM: "NONE",
374
+ BUTTON_LETTER_SPACING: 0,
375
+ // colours — unset follows the section's colour scheme
376
+ BUTTON_BG_COLOR: null,
377
+ BUTTON_TEXT_COLOR: null,
378
+ BUTTON_BORDER_COLOR: null,
379
+ BUTTON_HOVER_EFFECT: "FADE", // FADE | DARKEN | INVERT | LIFT | NONE
380
+ BUTTON_HOVER_BG_COLOR: null,
381
+ BUTTON_HOVER_TEXT_COLOR: null,
382
+ BUTTON_HOVER_BORDER_COLOR: null,
383
+ BUTTON_HOVER_SHADOW: null,
384
+ SECONDARY_BUTTON_BG_COLOR: null,
385
+ SECONDARY_BUTTON_TEXT_COLOR: null,
386
+ SECONDARY_BUTTON_BORDER_COLOR: null,
387
+ },
388
+
389
+ // Every storefront field: same controls CONTACT_FORM has per section.
390
+ INPUTS: {
391
+ INPUT_STYLE: "OUTLINE", // OUTLINE | FILLED | UNDERLINE
392
+ INPUT_LABEL_STYLE: "FLOATING", // FLOATING | ABOVE | PLACEHOLDER
393
+ INPUT_SHOW_REQUIRED_MARK: true,
394
+ INPUT_RADIUS: 4,
395
+ INPUT_BORDER_WIDTH: 1,
396
+ INPUT_SHADOW: "NONE",
397
+ INPUT_SHADOW_COLOR: null,
398
+ INPUT_PADDING_Y: 10,
399
+ INPUT_PADDING_X: 12,
400
+ INPUT_PADDING_Y_MOBILE: null,
401
+ INPUT_PADDING_X_MOBILE: null,
402
+ INPUT_FONT_SIZE: 15,
403
+ INPUT_FONT_SIZE_MOBILE: null,
404
+ INPUT_GAP: 16,
405
+ INPUT_BG_COLOR: null,
406
+ INPUT_TEXT_COLOR: null,
407
+ INPUT_BORDER_COLOR: null,
408
+ INPUT_FOCUS_COLOR: null,
409
+ INPUT_PLACEHOLDER_COLOR: null,
410
+ INPUT_ERROR_COLOR: null,
411
+ INPUT_LABEL_SIZE: 12,
412
+ INPUT_LABEL_WEIGHT: "MEDIUM",
413
+ INPUT_LABEL_TRANSFORM: "NONE",
414
+ INPUT_LABEL_LETTER_SPACING: 0,
415
+ INPUT_LABEL_COLOR: null,
416
+ },
417
+
418
+ CART: {
419
+ CART_TYPE: "DRAWER",
420
+ CART_DRAWER_TITLE: "Your cart",
421
+ CART_DRAWER_SHOW_COUNT: true,
422
+ CART_DRAWER_WIDTH: 440,
423
+ CART_DRAWER_COLOR_SCHEME: null,
424
+ CART_DRAWER_BACKGROUND: null,
425
+ CART_DRAWER_TEXT_COLOR: null,
426
+ CART_DRAWER_EMPTY_TITLE: "Your cart is empty",
427
+ CART_DRAWER_EMPTY_TEXT: "Have a look around — we think you will find something you love.",
428
+ CART_DRAWER_EMPTY_BUTTON: "Continue shopping",
429
+ CART_DRAWER_EMPTY_URL: "/categories/all",
430
+ CART_DRAWER_FREE_SHIPPING: false,
431
+ CART_DRAWER_FREE_SHIPPING_THRESHOLD: 100,
432
+ CART_DRAWER_FREE_SHIPPING_MESSAGE: "You're {amount} away from free shipping",
433
+ CART_DRAWER_FREE_SHIPPING_SUCCESS: "You've got free shipping!",
434
+ CART_DRAWER_PROGRESS_COLOR: null,
435
+ CART_DRAWER_PROGRESS_DONE_COLOR: null,
436
+ CART_DRAWER_RECOMMENDATIONS: true,
437
+ CART_DRAWER_RECOMMENDATIONS_TITLE: "You may also like",
438
+ CART_DRAWER_RECOMMENDATIONS_PRODUCTS: [],
439
+ CART_DRAWER_RECOMMENDATIONS_LIMIT: 4,
440
+ CART_DRAWER_RECS_LAYOUT: "GRID",
441
+ CART_DRAWER_RECS_COLUMNS: 2,
442
+ CART_DRAWER_RECS_GAP: 12,
443
+ CART_DRAWER_RECS_IMAGE_RATIO: "PORTRAIT",
444
+ CART_DRAWER_RECS_IMAGE_SIZE: 72,
445
+ CART_DRAWER_RECS_IMAGE_RADIUS: 6,
446
+ CART_DRAWER_RECS_CARD_STYLE: "NONE",
447
+ CART_DRAWER_RECS_LINE_COLOR: null,
448
+ CART_DRAWER_RECS_CARD_RADIUS: 8,
449
+ CART_DRAWER_RECS_CARD_PADDING_X: 0,
450
+ CART_DRAWER_RECS_CARD_PADDING_Y: 0,
451
+ // Buttons: empty = the theme's (Theme settings → Buttons).
452
+ CART_DRAWER_CHECKOUT_STYLE: "THEME",
453
+ CART_DRAWER_EMPTY_BTN_STYLE: "THEME",
454
+ CART_DRAWER_RECS_ADD_STYLE: "THEME",
455
+ CART_DRAWER_QTY_SIZE: 30,
456
+ CART_DRAWER_RECS_CARD_BACKGROUND: null,
457
+ CART_DRAWER_RECS_PRICE_POSITION: "BELOW",
458
+ CART_DRAWER_RECS_NAME_LINES: 2,
459
+ CART_DRAWER_RECS_NAME_SIZE: 13,
460
+ CART_DRAWER_RECS_PRICE_SIZE: 13,
461
+ CART_DRAWER_RECS_PRICE_COLOR: null,
462
+ CART_DRAWER_RECS_SHOW_ADD: false,
463
+ CART_DRAWER_RECS_ADD_LABEL: "Add",
464
+ CART_DRAWER_SUBTOTAL_NOTE: "Taxes and shipping calculated at checkout.",
465
+ CART_DRAWER_CHECKOUT_LABEL: "Checkout",
466
+ CART_DRAWER_SHOW_CONTINUE: true,
467
+ },
468
+
469
+ // Set once for the store. Sections may override individual keys, but the
470
+ // default is that every card in the store looks the same.
471
+ PRODUCT_CARD: {
472
+ // structure — these three are the ones that also produce a class
473
+ CARD_TEXT_POSITION: "BELOW", // BELOW | OVERLAY_BOTTOM | OVERLAY_HOVER
474
+ CARD_CTA_STYLE: "HOVER_OVER_IMAGE", // HOVER_OVER_IMAGE | UNDER_CONTENT | NONE
475
+ CARD_WISHLIST_POSITION: "TOP_END", // TOP_END | TOP_START | WITH_TITLE | NONE
476
+
477
+ // image
478
+ CARD_IMAGE_RATIO: "PORTRAIT", // PORTRAIT | SQUARE | LANDSCAPE | NATURAL
479
+ CARD_IMAGE_FIT: "COVER", // COVER | CONTAIN
480
+ CARD_SHOW_SECOND_IMAGE_ON_HOVER: true,
481
+
482
+ // text
483
+ CARD_TEXT_ALIGN: "START", // START | CENTER | END
484
+ CARD_NAME_SIZE: 14,
485
+ CARD_PRICE_SIZE: 14,
486
+ CARD_CONTENT_GAP: 4,
487
+ CARD_CONTENT_PADDING: 12,
488
+
489
+ // chrome
490
+ CARD_BADGE_POSITION: "TOP_START", // TOP_START | TOP_END | BOTTOM_START
491
+ CARD_BORDER_RADIUS: 0,
492
+ CARD_BACKGROUND: null,
493
+ CARD_TEXT_COLOR: null,
494
+ CARD_OVERLAY_SCRIM: "linear-gradient(transparent, rgba(0,0,0,0.55))",
495
+ CARD_CTA_FULL_WIDTH: true,
496
+
497
+ // behaviour
498
+ CARD_SHOW_QUICK_SHOP: true,
499
+ CARD_SHOW_SWATCHES: true,
500
+
501
+ // escape hatch — scoped to the card and sanitised, see scopeCardCss()
502
+ CARD_CUSTOM_CSS: "",
503
+ },
504
+ };
505
+
506
+ /**
507
+ * Which stored keys belong to which group.
508
+ *
509
+ * Needed because the group name isn't always the key prefix: BRAND's keys are
510
+ * LOGO / FAVICON, CART's are CART_TYPE / SHOW_VENDOR, and PRODUCT_CARD's are
511
+ * CARD_*. Prefixes are matched with startsWith, so SIDEBAR_LOGO_WIDTH does not
512
+ * fall into BRAND — it doesn't start with "LOGO".
513
+ */
514
+ const GROUP_KEYS = {
515
+ BRAND: ["LOGO", "FAVICON"],
516
+ SEO: ["SEO_"],
517
+ SOCIAL_MEDIA: ["SOCIAL_"],
518
+ WHATSAPP: ["WHATSAPP_"],
519
+ ANIMATIONS: ["ANIMATION_"],
520
+ CUSTOM_CSS: ["CUSTOM_CSS"],
521
+ ANNOUNCEMENT: ["ANNOUNCEMENT_"],
522
+ HEADER: ["HEADER_"],
523
+ SIDEBAR: ["SIDEBAR_"],
524
+ FOOTER: ["FOOTER_"],
525
+ BUTTONS: ["BUTTON_", "SECONDARY_BUTTON_"],
526
+ INPUTS: ["INPUT_"],
527
+ CART: ["CART_", "SHOW_VENDOR"],
528
+ PRODUCT_CARD: ["CARD_"],
529
+ };
530
+
531
+ /**
532
+ * @param {string} group BRAND | ANNOUNCEMENT | HEADER | SIDEBAR | FOOTER | BUTTONS | INPUTS | CART | PRODUCT_CARD
533
+ * @returns {ComputedRef<object>} settings for that group, defaults filled in
534
+ */
535
+ export function useThemeGroup(group) {
536
+ const { theme } = useStorefront();
537
+ const fallback = DEFAULTS[group] ?? {};
538
+ const prefixes = GROUP_KEYS[group] ?? [];
539
+
540
+ return computed(() => {
541
+ const settings = theme.value ?? {};
542
+ const resolved = { ...fallback };
543
+
544
+ // Iterate the PAYLOAD, not the fallback.
545
+ //
546
+ // The reverse — for (key of Object.keys(fallback)) — turns DEFAULTS into an
547
+ // allow-list: any setting the backend sends that hasn't also been added
548
+ // here is silently dropped, so a new schema field renders as its hardcoded
549
+ // fallback and editing it in the builder appears to do nothing. DEFAULTS is
550
+ // meant to fill gaps in old themes, not to gate new ones.
551
+ for (const [key, value] of Object.entries(settings)) {
552
+ const belongs = key in fallback || prefixes.some((p) => key.startsWith(p));
553
+ if (!belongs) continue;
554
+ // An unset setting arrives as undefined or null; false and 0 are real
555
+ // values a merchant chose and must survive.
556
+ if (value !== undefined && value !== null) resolved[key] = value;
557
+ }
558
+
559
+ if (import.meta.dev) {
560
+ for (const key of Object.keys(settings)) {
561
+ if (!prefixes.some((p) => key.startsWith(p))) continue;
562
+ if (key in fallback) continue;
563
+ console.warn(
564
+ `[theme] ${key} has no entry in THEME_GROUP_DEFAULTS.${group}. It ` +
565
+ `still renders, but a store that hasn't saved it gets undefined ` +
566
+ `instead of the schema default.`
567
+ );
568
+ }
569
+ }
570
+
571
+ return resolved;
572
+ });
573
+ }
574
+
575
+ export { DEFAULTS as THEME_GROUP_DEFAULTS };
@@ -0,0 +1,87 @@
1
+ import CartServices from "#osi/services/CartServices";
2
+ import { useToast } from "vue-toastification";
3
+ import { useCart } from "./useCart";
4
+ import { useSidebar } from "./useSidebar";
5
+
6
+ export function useAddToCart() {
7
+ const router = useRouter();
8
+ const { fetchCart } = useCart();
9
+ const { toggleCartSidebar } = useSidebar();
10
+ const { cartType } = useStorefront();
11
+
12
+ function openCartUi() {
13
+ if (cartType.value === "PAGE") {
14
+ router.push("/cart");
15
+ } else {
16
+ toggleCartSidebar(true);
17
+ }
18
+ }
19
+
20
+ /** A real price somewhere: the range ("25 - 27") or the single variant's. */
21
+ function isPriced(product) {
22
+ const parts = String(product?.discounted_price_range ?? product?.price_range ?? "")
23
+ .split("-")
24
+ .map((v) => parseFloat(v))
25
+ .filter((n) => Number.isFinite(n) && n > 0);
26
+ if (parts.length) return true;
27
+ return (product?.product_variants ?? []).some((v) => Number(v.price) > 0);
28
+ }
29
+
30
+ async function handleAddToCart(product) {
31
+ if (!product) return;
32
+ // No price: not for sale yet — never added (the button says so too).
33
+ if (!isPriced(product)) return;
34
+
35
+ // If product is already added (from Quick Shop), just open UI
36
+ if (product.added) {
37
+ openCartUi();
38
+ return;
39
+ }
40
+
41
+ // If product has multiple variants, redirect to detail page
42
+ if (Array.isArray(product.product_variants) && product.product_variants.length > 1) {
43
+ const categorySlug = product.categories?.[0]?.category?.slugified_name || 'products';
44
+ router.push(`/${categorySlug}/${product.slugified_name}/`);
45
+ return;
46
+ }
47
+
48
+ try {
49
+ const variantId = product.product_variants?.[0]?.id;
50
+ const quantityStep = product.quantity_step || 1;
51
+
52
+ if (!variantId) {
53
+ throw new Error("No variant available for this product.");
54
+ }
55
+
56
+ const response = await CartServices.EditCart(variantId, quantityStep, "add", {
57
+ product,
58
+ variant: product.product_variants?.[0],
59
+ });
60
+ if (response) {
61
+ await fetchCart();
62
+ openCartUi();
63
+ }
64
+ } catch (error) {
65
+ console.error("[useAddToCart] Error adding to cart:", error);
66
+ try {
67
+ const toast = useToast();
68
+ const data = error?.data;
69
+
70
+ if (Array.isArray(data) && data.length > 0) {
71
+ toast.error(data.length === 1 ? data[0] : data.map(e => `• ${e}`).join('\n'));
72
+ } else if (data && typeof data === 'object') {
73
+ const messages = Object.values(data).flat();
74
+ toast.error(messages.length === 1 ? messages[0] : messages.map(m => `• ${m}`).join('\n'));
75
+ } else {
76
+ toast.error(error?.message || 'An error occurred while adding to cart.');
77
+ }
78
+ } catch {
79
+ /* ignore toast context error */
80
+ }
81
+ }
82
+ }
83
+
84
+ return {
85
+ handleAddToCart,
86
+ };
87
+ }
@@ -0,0 +1,15 @@
1
+ import { analytics } from "#osi/utils/analytics";
2
+
3
+ /**
4
+ * Analytics for components and composables.
5
+ *
6
+ * const analytics = useAnalytics();
7
+ * analytics.viewItem(product);
8
+ *
9
+ * Safe everywhere: on the server, in the page-builder preview, and in a store
10
+ * with no tracking IDs every method is a silent no-op, so callers never check.
11
+ * See utils/analytics.js for the event list and how each maps to GA4 and Meta.
12
+ */
13
+ export function useAnalytics() {
14
+ return analytics;
15
+ }