@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,219 @@
1
+ /**
2
+ * Navigation settings -> CSS custom properties.
3
+ *
4
+ * Same contract as useThemeTokens: nothing in a template reads
5
+ * `settings.HEADER_MENU_GAP` directly, it reads `var(--osi-nav-gap)`. That's
6
+ * what lets the schema gain a field and the storefront follow without a
7
+ * component learning its name.
8
+ *
9
+ * Both functions read through useThemeGroup(), which falls back to its own
10
+ * DEFAULTS for any key the merchant's saved theme predates — so a theme
11
+ * installed before these settings existed renders the new defaults instead of
12
+ * `undefined` leaking into a style attribute.
13
+ */
14
+
15
+ const FONT_VAR = {
16
+ HEADING: "var(--osi-font-heading)",
17
+ BODY: "var(--osi-font-body)",
18
+ };
19
+
20
+ const WEIGHT = {
21
+ LIGHT: 300,
22
+ REGULAR: 400,
23
+ MEDIUM: 500,
24
+ SEMIBOLD: 600,
25
+ BOLD: 700,
26
+ };
27
+
28
+ /** Accepts 'MEDIUM' or a raw 500, because enums get refactored. */
29
+ function weightOf(value, fallback = 400) {
30
+ if (typeof value === "number") return value;
31
+ return WEIGHT[value] ?? (Number(value) || fallback);
32
+ }
33
+
34
+ function fontOf(role) {
35
+ return FONT_VAR[role] ?? FONT_VAR.BODY;
36
+ }
37
+
38
+ function transformOf(value) {
39
+ return !value || value === "NONE" ? "none" : String(value).toLowerCase();
40
+ }
41
+
42
+ /** Letter spacing is stored in hundredths of an em, matching text_style(). */
43
+ function trackingOf(value) {
44
+ return `${(Number(value) || 0) / 100}em`;
45
+ }
46
+
47
+ /** #rrggbb + 0-100 -> rgba(). Hex only; the COLOR field never emits anything else. */
48
+ function withAlpha(hex, percent) {
49
+ const alpha = Math.max(0, Math.min(100, Number(percent) ?? 100)) / 100;
50
+ const match = /^#?([\da-f]{6})$/i.exec(String(hex ?? ""));
51
+ if (!match) return `rgba(0, 0, 0, ${alpha})`;
52
+ const int = parseInt(match[1], 16);
53
+ return `rgba(${(int >> 16) & 255}, ${(int >> 8) & 255}, ${int & 255}, ${alpha})`;
54
+ }
55
+
56
+ /**
57
+ * Header inline navigation.
58
+ *
59
+ * Returns the variables plus the modifier classes for hover and active style,
60
+ * so SiteHeader binds one object and one array rather than assembling either.
61
+ */
62
+ export function useHeaderNavStyle() {
63
+ const settings = useThemeGroup("HEADER");
64
+
65
+ const style = computed(() => {
66
+ const s = settings.value;
67
+ return {
68
+ // The gap applies to the nav element only. The header grid's own
69
+ // `gap: 1rem` between logo / nav / icons is untouched — a merchant
70
+ // setting link spacing to 0 must not glue the logo to the menu.
71
+ "--osi-nav-gap": `${s.HEADER_MENU_GAP}px`,
72
+ "--osi-nav-gap-row": `${s.HEADER_MENU_GAP_MOBILE}px`,
73
+ "--osi-nav-font": fontOf(s.HEADER_MENU_FONT),
74
+ "--osi-nav-size": `${s.HEADER_MENU_SIZE}px`,
75
+ "--osi-nav-weight": weightOf(s.HEADER_MENU_WEIGHT),
76
+ "--osi-nav-color": s.HEADER_MENU_COLOR || "var(--osi-text)",
77
+ "--osi-nav-hover-color": s.HEADER_MENU_HOVER_COLOR || "var(--osi-nav-color)",
78
+ "--osi-nav-tracking": trackingOf(s.HEADER_MENU_LETTER_SPACING),
79
+ "--osi-nav-transform": transformOf(s.HEADER_MENU_TRANSFORM),
80
+ "--osi-nav-active-color": s.HEADER_MENU_ACTIVE_COLOR || "var(--osi-nav-color)",
81
+ "--osi-nav-active-bg": s.HEADER_MENU_ACTIVE_BACKGROUND || "transparent",
82
+ "--osi-nav-underline-w": `${s.HEADER_MENU_ACTIVE_UNDERLINE_WIDTH}px`,
83
+ "--osi-nav-underline-offset": `${s.HEADER_MENU_ACTIVE_UNDERLINE_OFFSET}px`,
84
+ // Submenu (dropdown / mega menu) colours: the header's colour scheme
85
+ // unless the Menu block sets its own.
86
+ "--osi-dd-bg": s.HEADER_MENU_DROPDOWN_BG || "var(--osi-bg, #fff)",
87
+ "--osi-dd-text": s.HEADER_MENU_DROPDOWN_COLOR || "var(--osi-text, #111)",
88
+ ...(s.HEADER_MENU_DROPDOWN_HOVER_BG ? { "--osi-dd-hover-bg": s.HEADER_MENU_DROPDOWN_HOVER_BG } : {}),
89
+ ...(s.HEADER_MENU_DROPDOWN_HOVER_COLOR ? { "--osi-dd-hover-color": s.HEADER_MENU_DROPDOWN_HOVER_COLOR } : {}),
90
+ };
91
+ });
92
+
93
+ const navClass = computed(() => [
94
+ `osi-nav--hover-${String(settings.value.HEADER_MENU_HOVER_EFFECT).toLowerCase()}`,
95
+ `osi-nav--active-${String(settings.value.HEADER_MENU_ACTIVE_STYLE).toLowerCase()}`,
96
+ ]);
97
+
98
+ const buttonStyle = computed(() => {
99
+ const s = settings.value;
100
+ return {
101
+ "--osi-menu-btn-size": `${s.HEADER_MENU_BUTTON_SIZE}px`,
102
+ "--osi-menu-btn-radius": `${s.HEADER_MENU_BUTTON_RADIUS}px`,
103
+ "--osi-menu-btn-bg": s.HEADER_MENU_BUTTON_BACKGROUND || "transparent",
104
+ "--osi-menu-btn-hover-bg":
105
+ s.HEADER_MENU_BUTTON_HOVER_BACKGROUND || "var(--osi-menu-btn-bg)",
106
+ };
107
+ });
108
+
109
+ return { style, navClass, buttonStyle };
110
+ }
111
+
112
+ /**
113
+ * Which nav item represents the page we're on.
114
+ *
115
+ * Prefix match, not equality: /categories/shoes/boots should light up the
116
+ * "Shoes" link. Guarded against '/' , which prefixes everything and would
117
+ * otherwise mark the Home link active on every page of the store.
118
+ */
119
+ export function useNavActive() {
120
+ const route = useRoute();
121
+
122
+ function isActive(item) {
123
+ if (!item?.url || item.external) return false;
124
+ const path = route.path.replace(/\/+$/, "") || "/";
125
+ const url = item.url.replace(/\/+$/, "") || "/";
126
+ if (url === "/") return path === "/";
127
+ if (path === url) return true;
128
+ if (path.startsWith(`${url}/`)) return true;
129
+ return (item.children ?? []).some(isActive);
130
+ }
131
+
132
+ return { isActive };
133
+ }
134
+
135
+ /**
136
+ * Sidebar / mobile navigation.
137
+ *
138
+ * Scheme first, overrides on top — the rule the header already follows. An
139
+ * override left empty resolves to the scheme's variable rather than to a
140
+ * hardcoded colour, so editing one palette still restyles the drawer.
141
+ */
142
+ export function useSidebarStyle() {
143
+ const settings = useThemeGroup("SIDEBAR");
144
+ const scheme = useColorScheme(computed(() => settings.value.SIDEBAR_COLOR_SCHEME));
145
+
146
+ const enabled = computed(() => settings.value.SIDEBAR_ENABLED !== false);
147
+ const position = computed(() => settings.value.SIDEBAR_POSITION || "START");
148
+ const animation = computed(() => settings.value.SIDEBAR_ANIMATION || "SLIDE");
149
+ const submenuMode = computed(() => settings.value.SIDEBAR_SUBMENU_MODE || "ACCORDION");
150
+
151
+ const duration = computed(() =>
152
+ animation.value === "NONE" ? 0 : Number(settings.value.SIDEBAR_ANIMATION_DURATION) || 0
153
+ );
154
+
155
+ console.log("animation",animation.value)
156
+ console.log("duration",duration.value);
157
+ const style = computed(() => {
158
+ const s = settings.value;
159
+ const base = { ...scheme.value.style };
160
+
161
+ if (s.SIDEBAR_BACKGROUND) base["--osi-bg"] = s.SIDEBAR_BACKGROUND;
162
+ if (s.SIDEBAR_TEXT_COLOR) base["--osi-text"] = s.SIDEBAR_TEXT_COLOR;
163
+ if (s.SIDEBAR_BORDER_COLOR) base["--osi-border"] = s.SIDEBAR_BORDER_COLOR;
164
+
165
+ return {
166
+ ...base,
167
+
168
+ "--osi-sb-width": `${s.SIDEBAR_WIDTH}px`,
169
+ "--osi-sb-width-mobile": `${s.SIDEBAR_WIDTH_MOBILE}vw`,
170
+ "--osi-sb-duration": `${duration.value}ms`,
171
+ "--osi-sb-radius": `${s.SIDEBAR_RADIUS}px`,
172
+ "--osi-sb-border-width": s.SIDEBAR_SHOW_BORDER ? `${s.SIDEBAR_BORDER_WIDTH}px` : "0px",
173
+
174
+ "--osi-sb-muted": s.SIDEBAR_MUTED_COLOR || "color-mix(in srgb, var(--osi-text) 60%, transparent)",
175
+ "--osi-sb-link": s.SIDEBAR_LINK_COLOR || "var(--osi-text)",
176
+ "--osi-sb-link-hover": s.SIDEBAR_LINK_HOVER_COLOR || "var(--osi-sb-link)",
177
+ "--osi-sb-active": s.SIDEBAR_ACTIVE_COLOR || "var(--osi-sb-link)",
178
+ "--osi-sb-active-bg": s.SIDEBAR_ACTIVE_BACKGROUND || "transparent",
179
+ "--osi-sb-icon": s.SIDEBAR_ICON_COLOR || "var(--osi-text)",
180
+
181
+ "--osi-sb-pad-top": `${s.SIDEBAR_PADDING_TOP}px`,
182
+ "--osi-sb-pad-bottom": `${s.SIDEBAR_PADDING_BOTTOM}px`,
183
+ "--osi-sb-pad-x": `${s.SIDEBAR_PADDING_X}px`,
184
+ "--osi-sb-link-gap": `${s.SIDEBAR_LINK_GAP}px`,
185
+ "--osi-sb-link-pad-y": `${s.SIDEBAR_LINK_PADDING_Y}px`,
186
+ "--osi-sb-group-gap": `${s.SIDEBAR_GROUP_GAP}px`,
187
+ "--osi-sb-indent": `${s.SIDEBAR_SUBMENU_INDENT}px`,
188
+
189
+ "--osi-sb-font": fontOf(s.SIDEBAR_LINK_FONT),
190
+ "--osi-sb-size": `${s.SIDEBAR_LINK_SIZE}px`,
191
+ "--osi-sb-size-mobile": `${s.SIDEBAR_LINK_SIZE_MOBILE}px`,
192
+ "--osi-sb-weight": weightOf(s.SIDEBAR_LINK_WEIGHT, 500),
193
+ "--osi-sb-line-height": (Number(s.SIDEBAR_LINK_LINE_HEIGHT) || 130) / 100,
194
+ "--osi-sb-tracking": trackingOf(s.SIDEBAR_LINK_LETTER_SPACING),
195
+ "--osi-sb-transform": transformOf(s.SIDEBAR_LINK_TRANSFORM),
196
+
197
+ "--osi-sb-sub-size": `${s.SIDEBAR_SUBMENU_SIZE}px`,
198
+ "--osi-sb-sub-weight": weightOf(s.SIDEBAR_SUBMENU_WEIGHT),
199
+ "--osi-sb-sub-color": s.SIDEBAR_SUBMENU_COLOR || "var(--osi-sb-link)",
200
+
201
+ "--osi-sb-arrow-size": `${s.SIDEBAR_ARROW_SIZE}px`,
202
+ "--osi-sb-header-height": `${s.SIDEBAR_HEADER_HEIGHT}px`,
203
+ "--osi-sb-logo-width": `${s.SIDEBAR_LOGO_WIDTH}px`,
204
+ };
205
+ });
206
+
207
+ const overlayStyle = computed(() => ({
208
+ background: withAlpha(
209
+ settings.value.SIDEBAR_OVERLAY_COLOR || "#000000",
210
+ settings.value.SIDEBAR_OVERLAY_OPACITY
211
+ ),
212
+ transitionDuration: `${duration.value}ms`,
213
+ }));
214
+
215
+ return {
216
+ settings, scheme, style, overlayStyle,
217
+ enabled, position, animation, duration, submenuMode,
218
+ };
219
+ }
@@ -0,0 +1,294 @@
1
+ // app/composables/usePlaceholders.js
2
+ //
3
+ // Dummy products and classifications for a store whose catalog is still empty,
4
+ // so the builder shows a laid-out page instead of blank rows.
5
+ //
6
+ // Scope is exactly those two. Menus are real rows seeded at store creation, so
7
+ // nothing here touches them, and section media is left alone — a hero with no
8
+ // image is the merchant's own empty state to fill.
9
+ //
10
+ // Three rules hold this together:
11
+ //
12
+ // 1. Rendered, never stored. Nothing here is a Product, Category or Menu row.
13
+ // It cannot reach checkout, search, the sitemap, an export, or a save — the
14
+ // builder posts its own state back, and this never enters that state.
15
+ //
16
+ // 2. Builder iframe only. The gate is usePreviewOverlay().active, which
17
+ // useBuilderPreview.start() sets after confirming window.parent !== window.
18
+ // A live storefront with an empty catalog shows a real empty state, and an
19
+ // unpublished theme opened directly in a browser does too.
20
+ //
21
+ // 3. Raw shapes, not normalized ones. A placeholder category is shaped like a
22
+ // row out of the categories endpoint, and a placeholder product like one out
23
+ // of ProductListSerializer. They enter at useSectionData, upstream of
24
+ // useClassificationItems and ProductCard, so every consumer downstream —
25
+ // PARENTS_ONLY, LIMIT, HIDE_EMPTY, the URL table, the card styling — runs
26
+ // over them unchanged. Injecting normalized items instead would mean each of
27
+ // those had to learn about placeholders.
28
+ //
29
+ // Copy comes from the theme payload (default_theme.json -> `placeholders`) when
30
+ // it's there, so wording can improve server-side without a storefront deploy.
31
+ // The lists below are the fallback for a theme that doesn't carry the key.
32
+
33
+ import { computed } from "vue";
34
+
35
+ /**
36
+ * When empty lists get filled.
37
+ *
38
+ * PREVIEW only when the storefront is framed by the dashboard
39
+ * ALWAYS any time a list comes back empty, including on the real URL
40
+ *
41
+ * PREVIEW is the point of this file: a shopper on a live store must never see
42
+ * invented merchandise. ALWAYS exists for debugging the placeholder rendering
43
+ * itself outside the builder — it is not a mode to ship.
44
+ */
45
+ const WHEN_EMPTY = "PREVIEW";
46
+
47
+ const FALLBACK = {
48
+ products: [
49
+ { title: "Product title 1", price: 2900, compare_at_price: null },
50
+ { title: "Product title 2", price: 8900, compare_at_price: 11000 },
51
+ { title: "Product title 3", price: 4500, compare_at_price: null },
52
+ { title: "Product title 4", price: 12000, compare_at_price: null },
53
+ { title: "Product title 5", price: 6500, compare_at_price: 8000 },
54
+ { title: "Product title 6", price: 3800, compare_at_price: null },
55
+ { title: "Product title 7", price: 15000, compare_at_price: null },
56
+ { title: "Product title 8", price: 9500, compare_at_price: null },
57
+ ],
58
+ classifications: [
59
+ { title: "Category title 1" },
60
+ { title: "Category title 2" },
61
+ { title: "Category title 3" },
62
+ { title: "Category title 4" },
63
+ { title: "Category title 5" },
64
+ { title: "Category title 6" },
65
+ ],
66
+ currency: "USD",
67
+ price_divisor: 100,
68
+ };
69
+
70
+ /**
71
+ * The art on a placeholder card.
72
+ *
73
+ * An inline SVG rather than a remote URL: the preview runs in an iframe, and a
74
+ * placeholder that depends on the network is a placeholder that sometimes
75
+ * renders as a broken-image icon. useMediaUrl passes `data:` straight through,
76
+ * so this drops into any `:src` that expects a media path.
77
+ *
78
+ * The hue shifts per index so a grid of six doesn't read as one tile repeated.
79
+ */
80
+ export function placeholderImage({ w = 1200, h = 900, index = 0, tone = "light" } = {}) {
81
+ const hue = (index * 47 + 210) % 360;
82
+ const bg = tone === "dark" ? `hsl(${hue} 14% 20%)` : `hsl(${hue} 18% 93%)`;
83
+ const fg = tone === "dark" ? `hsl(${hue} 12% 32%)` : `hsl(${hue} 16% 80%)`;
84
+
85
+ const svg =
86
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}">` +
87
+ `<rect width="${w}" height="${h}" fill="${bg}"/>` +
88
+ `<g fill="${fg}">` +
89
+ `<circle cx="${w * 0.34}" cy="${h * 0.32}" r="${Math.min(w, h) * 0.072}"/>` +
90
+ `<path d="M${w * 0.08} ${h * 0.82} L${w * 0.38} ${h * 0.42} L${w * 0.56} ${h * 0.64} ` +
91
+ `L${w * 0.7} ${h * 0.51} L${w * 0.92} ${h * 0.82} Z"/>` +
92
+ `</g></svg>`;
93
+
94
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
95
+ }
96
+
97
+ export function usePlaceholders() {
98
+ // Read the bootstrap state directly rather than through useStorefront(),
99
+ // which would pull in the preview overlay merge for a value we only need one
100
+ // key out of.
101
+ const data = useState("storefront", () => null);
102
+
103
+ const { active: isPreview } = usePreviewOverlay();
104
+ const route = useRoute();
105
+
106
+ /**
107
+ * Is this page being rendered inside the dashboard?
108
+ *
109
+ * Three signals, because the obvious one alone isn't enough:
110
+ *
111
+ * isPreview usePreviewOverlay().active, set by useBuilderPreview
112
+ * .start(). Authoritative, but it only flips once the READY
113
+ * handshake has been set up in app.vue's onMounted — so it
114
+ * is false during SSR and for the first moments after
115
+ * hydration, and any section that resolved its rows before
116
+ * then saw an empty catalog with placeholders switched off.
117
+ *
118
+ * ?osi_preview a query param on the iframe src. The only signal that
119
+ * exists during SSR, so it's the one that makes the first
120
+ * paint correct instead of empty-then-filled. Optional —
121
+ * add it to the iframe URL in the dashboard when you want
122
+ * the flash gone.
123
+ *
124
+ * window.parent the same framing test start() makes, done directly so it
125
+ * doesn't wait on the handshake. Client-only by nature.
126
+ */
127
+ const framed = computed(() => {
128
+ if (isPreview.value === true) return true;
129
+ if (
130
+ route?.query?.osi_preview !== undefined ||
131
+ route?.query?.preview !== undefined ||
132
+ route?.query?.builder !== undefined ||
133
+ route?.path?.includes("__builder-preview")
134
+ ) {
135
+ return true;
136
+ }
137
+ if (import.meta.client) {
138
+ try {
139
+ return window.self !== window.top || window.parent !== window;
140
+ } catch {
141
+ return true;
142
+ }
143
+ }
144
+ return false;
145
+ });
146
+
147
+ const allowed = computed(() => WHEN_EMPTY === "ALWAYS" || framed.value);
148
+
149
+ /**
150
+ * Theme copy wins per key, and only when it actually has rows.
151
+ *
152
+ * A plain spread was wrong: a theme shipping `placeholders: { products: [] }`
153
+ * overrode the fallback with nothing, and the grid stayed empty with no error
154
+ * anywhere. An empty list in the payload means "nothing to say here", not
155
+ * "render nothing".
156
+ */
157
+ const source = computed(() => {
158
+ const custom = data.value?.theme?.placeholders ?? {};
159
+ return {
160
+ ...FALLBACK,
161
+ ...custom,
162
+ products: custom.products?.length ? custom.products : FALLBACK.products,
163
+ classifications: custom.classifications?.length
164
+ ? custom.classifications
165
+ : FALLBACK.classifications,
166
+ };
167
+ });
168
+
169
+ const divisor = computed(() => Number(source.value.price_divisor) || 100);
170
+
171
+ /** Shaped like a row from the categories endpoint. */
172
+ function asClassificationRow(raw, index) {
173
+ return {
174
+ is_placeholder: true,
175
+ type: "CATEGORY",
176
+ id: `ph-cat-${index}`,
177
+ name: raw.title,
178
+ // No slug: classificationUrl falls through to "/" rather than building a
179
+ // path that would 404 the moment someone opened the preview in a tab.
180
+ slugified_name: null,
181
+ url: null,
182
+ featured: false,
183
+ image: { path: placeholderImage({ w: 900, h: 700, index }) },
184
+ order: index + 1,
185
+ parent_category: null,
186
+ // A real number, not 0: HIDE_EMPTY would filter the whole set away.
187
+ products_count: raw.products_count ?? 12,
188
+ };
189
+ }
190
+
191
+ /** Shaped like a row from ProductListSerializer. */
192
+ function asProductRow(raw, index) {
193
+ const media = { id: null, path: placeholderImage({ w: 800, h: 1000, index }) };
194
+ const price = Number(raw.price ?? 0) / divisor.value;
195
+ const compare = raw.compare_at_price
196
+ ? Number(raw.compare_at_price) / divisor.value
197
+ : null;
198
+
199
+ return {
200
+ is_placeholder: true,
201
+ id: `ph-prod-${index}`,
202
+ name: raw.title,
203
+ page_title: raw.title,
204
+ slugified_name: null,
205
+ url: null,
206
+ description: "",
207
+ status: "In Stock",
208
+ stock: null,
209
+ remaining_stock: 25,
210
+ quantity_unit: null,
211
+ quantity_step: 1,
212
+ sku: "",
213
+ price_range: String(compare ?? price),
214
+ discounted_price_range: compare ? String(price) : null,
215
+ main_image: media,
216
+ gallery: [{ id: null, media, order: 0, product: `ph-prod-${index}` }],
217
+ categories: [],
218
+ collections: [],
219
+ resources: [],
220
+ sections: [],
221
+ brand: null,
222
+ vary_by: [],
223
+ wishlisted: false,
224
+ product_variants: [
225
+ {
226
+ id: `ph-var-${index}`,
227
+ price,
228
+ compare_at_price: compare,
229
+ discounted_price_range: compare ? String(price) : null,
230
+ cost: null,
231
+ values: [],
232
+ gallery: [],
233
+ remaining_stock: 25,
234
+ discount: null,
235
+ sku: "",
236
+ max_quantity_per_user: null,
237
+ // False on purpose: a placeholder must never render as sold out, and
238
+ // the card reads stock off the variant.
239
+ track_inventory: false,
240
+ coming_soon: false,
241
+ restock_threshold: null,
242
+ allow_restock_notifications: false,
243
+ unlisted_at: null,
244
+ archived_at: null,
245
+ resources: [],
246
+ sales: [],
247
+ },
248
+ ],
249
+ published_at: null,
250
+ unlisted_at: null,
251
+ archived_at: null,
252
+ page_views: null,
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Repeats the sample set when a section asks for more than we ship, so
258
+ * LIMIT: 12 doesn't silently render 8 and make the grid look broken.
259
+ */
260
+ function take(list, count, map) {
261
+ if (!list?.length || count <= 0) {
262
+ if (import.meta.dev && count > 0) {
263
+ console.warn(
264
+ "[usePlaceholders] nothing to render: the sample list is empty. " +
265
+ "Check theme.placeholders in the bootstrap payload."
266
+ );
267
+ }
268
+ return [];
269
+ }
270
+ return Array.from({ length: count }, (_, i) => map(list[i % list.length], i));
271
+ }
272
+
273
+ /**
274
+ * @param {Array} real what the backend resolved
275
+ * @param {number} count how many the section wants
276
+ * @returns the real rows when there are any, placeholders when there aren't
277
+ * and we're in the editor, an empty array otherwise
278
+ */
279
+ const products = (real, count) =>
280
+ real?.length
281
+ ? real
282
+ : allowed.value
283
+ ? take(source.value.products, count, asProductRow)
284
+ : [];
285
+
286
+ const classifications = (real, count) =>
287
+ real?.length
288
+ ? real
289
+ : allowed.value
290
+ ? take(source.value.classifications, count, asClassificationRow)
291
+ : [];
292
+
293
+ return { products, classifications, allowed };
294
+ }
@@ -0,0 +1,26 @@
1
+ import PoliciesServices from "#osi/services/PoliciesServices";
2
+
3
+ export function usePolicies() {
4
+ const policies = useState("store-policies-data", () => ({}));
5
+ const loading = useState("store-policies-loading", () => false);
6
+
7
+ async function fetchPolicies() {
8
+ if (Object.keys(policies.value).length > 0) return policies.value;
9
+ loading.value = true;
10
+ try {
11
+ const data = await PoliciesServices.getPolicies();
12
+ policies.value = data || {};
13
+ } catch (error) {
14
+ console.error("[usePolicies] Error loading policies:", error);
15
+ } finally {
16
+ loading.value = false;
17
+ }
18
+ return policies.value;
19
+ }
20
+
21
+ return {
22
+ policies,
23
+ loading,
24
+ fetchPolicies,
25
+ };
26
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Which images a card shows, and the carousel around them.
3
+ *
4
+ * The image set is derived, not stored. It changes when quick-shop opens, when
5
+ * a variant is picked, and when a colour swatch is clicked — Terazar's
6
+ * `currentImages` computed handled all three and it's lifted whole here,
7
+ * because getting it subtly wrong shows the wrong colourway to a shopper.
8
+ *
9
+ * The three branches, in the order they're checked:
10
+ *
11
+ * 1. quick-shop open -> the selected variant's gallery; failing that, the
12
+ * galleries of every variant matching any current selection; failing that,
13
+ * the product's own main image + gallery
14
+ * 2. a colour selected on the closed card -> that colour's variant gallery,
15
+ * or the main image alone
16
+ * 3. nothing selected -> main image + gallery
17
+ *
18
+ * `__mediaKey` exists because the same media object can arrive through both a
19
+ * variant gallery and the product gallery; without deduping, the carousel shows
20
+ * the same photo twice and the dots lie about how many images there are.
21
+ */
22
+ export function useProductCardMedia({
23
+ product,
24
+ quickShopOpen,
25
+ selectedVariant,
26
+ selectedOptions,
27
+ selectedColor,
28
+ }) {
29
+ const p = computed(() => toValue(product) ?? {});
30
+ const open = computed(() => !!toValue(quickShopOpen));
31
+
32
+ const { mediaUrl, posterUrl, mediaUrlAs } = useMediaUrl();
33
+ const { isImage, isVideo } = useMediaTypes();
34
+
35
+ const isHovering = ref(false);
36
+
37
+ const key = (media) => media?.id || media?.path;
38
+ const wrap = (media) => ({ ...media, __mediaKey: key(media) });
39
+
40
+ function productOwnImages() {
41
+ const images = [];
42
+ if (p.value.main_image) images.push(wrap(p.value.main_image));
43
+ if (p.value.gallery?.length) {
44
+ images.push(...p.value.gallery.map((g) => wrap(g.media)));
45
+ }
46
+ return images;
47
+ }
48
+
49
+ const currentImages = computed(() => {
50
+ let images = [];
51
+ const variant = toValue(selectedVariant);
52
+ const options = toValue(selectedOptions) ?? {};
53
+ const colour = toValue(selectedColor);
54
+
55
+ if (open.value) {
56
+ if (variant?.gallery?.length) {
57
+ images = variant.gallery.map((g) => wrap(g.media));
58
+ } else {
59
+ const selectedValues = Object.values(options);
60
+
61
+ if (selectedValues.length) {
62
+ const matching =
63
+ p.value.product_variants?.filter((v) =>
64
+ v.values.some((val) => selectedValues.includes(val.id))
65
+ ) ?? [];
66
+
67
+ matching.forEach((v) => {
68
+ v.gallery?.forEach((g) => images.push(wrap(g.media)));
69
+ });
70
+ }
71
+
72
+ if (!images.length) images = productOwnImages();
73
+ }
74
+ } else if (colour) {
75
+ const variantForColour = p.value.product_variants?.find((v) =>
76
+ v.values.some((val) => val.id === colour.id)
77
+ );
78
+
79
+ images = variantForColour?.gallery?.length
80
+ ? variantForColour.gallery.map((g) => wrap(g.media))
81
+ : p.value.main_image
82
+ ? [wrap(p.value.main_image)]
83
+ : [];
84
+ } else {
85
+ images = productOwnImages();
86
+ }
87
+
88
+ const seen = new Set();
89
+ return images.filter((image) => {
90
+ if (!image.__mediaKey || seen.has(image.__mediaKey)) return false;
91
+ seen.add(image.__mediaKey);
92
+ return true;
93
+ });
94
+ });
95
+
96
+ /** The second image, revealed on hover when the theme asks for it. */
97
+ const hoverImage = computed(() => currentImages.value[1] ?? null);
98
+
99
+ const carousel = useCarousel({
100
+ items: () => currentImages.value,
101
+ loop: () => currentImages.value.length > 1,
102
+ minLoopSlides: 3,
103
+ });
104
+
105
+ // Advance to the first image only when a variant was actually picked — not
106
+ // every time currentImages recomputes. Opening quick-shop with nothing
107
+ // selected recomputes it too, and jumping the carousel then looks like a bug.
108
+ watch(
109
+ () => toValue(selectedVariant),
110
+ (variant) => {
111
+ nextTick(() => {
112
+ carousel.update();
113
+ if (variant && open.value && currentImages.value.length > 1) {
114
+ carousel.goTo(0);
115
+ }
116
+ });
117
+ }
118
+ );
119
+
120
+ const src = (media) => mediaUrl(media);
121
+ const poster = (media) => posterUrl(media, media?.poster);
122
+ const sourceAs = (media, extension) => mediaUrlAs(media, extension);
123
+
124
+ return {
125
+ currentImages,
126
+ hoverImage,
127
+ isHovering,
128
+ isImage,
129
+ isVideo,
130
+ src,
131
+ poster,
132
+ sourceAs,
133
+ ...carousel,
134
+ };
135
+ }