@osimart/create-storefront 0.0.0-stage → 1.0.1

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.js +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.js +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 +282 -0
  200. package/nuxt/osi-sections.ts +56 -0
  201. package/nuxt.config.ts +60 -0
  202. package/package.json +44 -4
  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,57 @@
1
+ import HomeServices from "#osi/services/HomeServices";
2
+ import { toPlainError } from "#osi/utils/toPlainError";
3
+
4
+ export function useStoreData() {
5
+ const products = useState("store-data-products", () => []);
6
+ const categories = useState("store-data-categories", () => []);
7
+ const collections = useState("store-data-collections", () => []);
8
+ const storeDataLoaded = useState("store-data-loaded", () => false);
9
+ const loadingStoreData = useState("store-data-loading", () => false);
10
+ const storeDataError = useState("store-data-error", () => null);
11
+ // Resolved here, in setup scope, before loadStoreData's own await boundary —
12
+ // see HomeServices.js for why this can't be left to resolve inside the
13
+ // Promise.allSettled calls below on the server.
14
+ const baseUrl = useState("base-url", () => "");
15
+
16
+ const loadStoreData = async () => {
17
+ if (storeDataLoaded.value || loadingStoreData.value) return;
18
+
19
+ loadingStoreData.value = true;
20
+ storeDataError.value = null;
21
+
22
+ try {
23
+ const [newArrivals, cats, cols] = await Promise.allSettled([
24
+ HomeServices.getNewArrivals(10, baseUrl.value),
25
+ HomeServices.getCategories(baseUrl.value),
26
+ HomeServices.getCollections(baseUrl.value),
27
+ ]);
28
+
29
+ products.value =
30
+ newArrivals.status === "fulfilled"
31
+ ? newArrivals.value?.results?.slice(5, 11) || []
32
+ : [];
33
+ categories.value =
34
+ cats.status === "fulfilled" ? cats.value?.results || [] : [];
35
+ collections.value =
36
+ cols.status === "fulfilled" ? cols.value?.results || [] : [];
37
+
38
+ storeDataLoaded.value = true;
39
+ } catch (err) {
40
+ console.error("[useStoreData] unexpected error:", err);
41
+ // Same reason as useStorefront: useState is serialized into the SSR
42
+ // payload, and an Error instance breaks it.
43
+ storeDataError.value = toPlainError(err);
44
+ } finally {
45
+ loadingStoreData.value = false;
46
+ }
47
+ };
48
+
49
+ return {
50
+ products,
51
+ categories,
52
+ collections,
53
+ loadingStoreData,
54
+ storeDataError,
55
+ loadStoreData,
56
+ };
57
+ }
@@ -0,0 +1,52 @@
1
+ import { storefrontService } from "#osi/services/storefront.service";
2
+
3
+ export function useStoreInit() {
4
+ const bootstrapData = useState('bootstrap-data', () => null);
5
+ const storeData = useState('store-data', () => null);
6
+ const baseUrl = useState('base-url', () => '');
7
+
8
+ const reqHeaders = import.meta.server ? useRequestHeaders(['host', 'x-forwarded-host', 'x-forwarded-proto']) : {};
9
+ const browserHost =
10
+ (import.meta.server
11
+ ? (reqHeaders['x-forwarded-host'] || reqHeaders.host)
12
+ : location.host) || '';
13
+
14
+ const browserProtocol =
15
+ (import.meta.server
16
+ ? (reqHeaders['x-forwarded-proto'] || 'https')
17
+ : location.protocol.replace(':', '')) || 'https';
18
+
19
+ // In dev, the storefront (this frontend) and the backend run on different
20
+ // ports/hosts, so we can't use the browser's own origin for API calls — it
21
+ // points at the frontend dev server. In production frontend and backend
22
+ // share the same origin, so the browser origin is correct.
23
+ const { storeApiBase } = useRuntimeConfig().public
24
+ const devHost = (browserHost || '').split(':')[0]
25
+
26
+ const API_ROOT = storeApiBase
27
+ || (import.meta.dev
28
+ ? (devHost ? `http://${devHost}:8000/` : 'http://test6.192.168.1.10.nip.io:8000/')
29
+ : (browserHost ? `${browserProtocol}://${browserHost}/api/` : ''));
30
+ async function init() {
31
+ if (bootstrapData.value && baseUrl.value) return bootstrapData.value;
32
+
33
+ baseUrl.value = API_ROOT;
34
+
35
+ try {
36
+ const data = await storefrontService.bootstrap({
37
+ baseUrl: API_ROOT,
38
+ host: browserHost,
39
+ });
40
+
41
+ bootstrapData.value = data;
42
+ storeData.value = data?.store ?? null;
43
+ console.log("storedataaaaa", bootstrapData.value);
44
+ } catch (error) {
45
+ console.error('[useStoreInit] Failed to fetch bootstrap data:', error);
46
+ }
47
+
48
+ return bootstrapData.value;
49
+ }
50
+
51
+ return { storeData, baseUrl, bootstrapData, init };
52
+ }
@@ -0,0 +1,239 @@
1
+ import { storefrontService } from "#osi/services/storefront.service";
2
+ import { toPlainError } from "#osi/utils/toPlainError";
3
+ import { cachedRequest } from "#osi/utils/requestCache";
4
+
5
+ const RTL_LOCALES = ["ar", "he", "fa", "ur"];
6
+
7
+ /** Section-group sections whose settings are (former) theme settings. */
8
+ const CHROME_SECTIONS = new Set(["ANNOUNCEMENT_BAR", "HEADER", "FOOTER", "FOOTER_UTILITIES"]);
9
+ /**
10
+ * Their blocks that hold former theme settings (same IDs) — laid over the
11
+ * theme too. The footer's other blocks have IDs of their own and are drawn
12
+ * from the block (FooterBlock).
13
+ */
14
+ const THEME_BLOCKS = new Set([
15
+ "HEADER_LOGO", "HEADER_MENU", "FOOTER_SOCIAL",
16
+ "UTILITY_COPYRIGHT", "UTILITY_POLICIES", "UTILITY_SOCIAL",
17
+ ]);
18
+
19
+ /** The Footer section's colours, which the Utilities bar follows. */
20
+ const FOOTER_APPEARANCE = [
21
+ "FOOTER_COLOR_SCHEME", "FOOTER_BACKGROUND", "FOOTER_TEXT_COLOR",
22
+ "FOOTER_LINK_COLOR", "FOOTER_LINK_HOVER_COLOR",
23
+ ];
24
+
25
+ /** Menus arrive as a map keyed by handle from bootstrap, and as a plain list
26
+ * from the dashboard's MENUS message. Normalize to the map either way. */
27
+ function menusAsMap(value) {
28
+ if (!value) return null;
29
+ if (!Array.isArray(value)) return value;
30
+ return Object.fromEntries(
31
+ value.filter((m) => m?.handle).map((m) => [m.handle, m])
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Root storefront state: theme settings, menus, store info, locale.
37
+ *
38
+ * Everything else reads from here. Loaded once during SSR in app.vue and shared
39
+ * for the rest of the render.
40
+ *
41
+ * useState, never a module-level ref: one Nuxt process serves every tenant, so
42
+ * a module-scoped ref leaks one store's theme into another shopper's page.
43
+ */
44
+ export function useStorefront() {
45
+ const data = useState("storefront", () => null);
46
+ const loading = useState("storefront-loading", () => false);
47
+ const error = useState("storefront-error", () => null);
48
+
49
+ // Unsaved edits from the builder iframe. Null outside the editor, so every
50
+ // computed below collapses to the saved values on a real storefront.
51
+ const { theme: themeOverlay, menus: menusOverlay, state: previewState } = usePreviewOverlay();
52
+
53
+ const store = computed(() => data.value?.store ?? null);
54
+
55
+ const savedTheme = computed(() => data.value?.theme?.settings ?? {});
56
+
57
+ /**
58
+ * Saved settings with the builder's unsaved edits layered on top.
59
+ *
60
+ * Merged, not replaced: the dashboard may push a single changed key over
61
+ * SETTING, and every key it doesn't mention has to keep its saved value —
62
+ * otherwise dragging a colour picker blanks the rest of the theme.
63
+ *
64
+ * This is the single point where "unsaved" enters the storefront. Anything
65
+ * reading theme settings — useThemeGroup, useColorScheme, useThemeTokens,
66
+ * useProductCardStyle — is live in the editor because of this computed, and
67
+ * needs no preview handling of its own.
68
+ */
69
+ /**
70
+ * Header / footer section groups — { header: [section…], footer: [...] }.
71
+ * The editor's (unsaved) when it has sent them, else the saved ones from
72
+ * bootstrap. null: a backend without groups — the layout falls back to the
73
+ * old fixed header and footer.
74
+ */
75
+ const sectionGroups = computed(
76
+ () => previewState.value?.groups ?? data.value?.section_groups ?? null
77
+ );
78
+
79
+ /**
80
+ * The header, footer and announcement bar sections' settings. They're the
81
+ * old theme settings, moved with the same IDs, so they're laid over the
82
+ * theme and every component keeps reading `theme.HEADER_*` etc.
83
+ */
84
+ const groupSettings = computed(() => {
85
+ const groups = sectionGroups.value;
86
+ if (!groups) return null;
87
+ // The editor also sends hidden sections (drawn dimmed); the saved payload
88
+ // holds live ones only.
89
+ const chrome = [...(groups.header ?? []), ...(groups.footer ?? [])].filter(
90
+ (s) => s && CHROME_SECTIONS.has(s.type)
91
+ );
92
+ const out = {};
93
+ for (const s of chrome) {
94
+ Object.assign(out, s.settings ?? {});
95
+ // Blocks holding their parts' settings (the Header's Logo / Menu…).
96
+ for (const b of s.blocks ?? []) {
97
+ if (!b.disabled && THEME_BLOCKS.has(b.type)) Object.assign(out, b.settings ?? {});
98
+ }
99
+ }
100
+ // Deleting the Logo or Menu block takes that part out of the header.
101
+ const header = chrome.find((s) => s.type === "HEADER");
102
+ if (header) {
103
+ const has = (type) => (header.blocks ?? []).some((b) => b.type === type && !b.disabled);
104
+ out.HEADER_SHOW_LOGO = has("HEADER_LOGO");
105
+ out.HEADER_SHOW_MENU = has("HEADER_MENU");
106
+ }
107
+ // The footer's appearance comes from the Footer section alone. Without
108
+ // one (only Utilities), the old theme-level values must not leak through
109
+ // (a stale FOOTER_COLOR_SCHEME pinned the bar to it): unset, so the bar
110
+ // falls back to the theme's Default scheme.
111
+ const footer = chrome.find((s) => s.type === "FOOTER");
112
+ for (const key of FOOTER_APPEARANCE) out[key] = footer?.settings?.[key] ?? null;
113
+
114
+ // Utilities: each part shows only while its block is there.
115
+ const utilities = chrome.find((s) => s.type === "FOOTER_UTILITIES");
116
+ const hasUtil = (type) => (utilities?.blocks ?? []).some((b) => b.type === type && !b.disabled);
117
+ out.FOOTER_UTIL_COPYRIGHT = hasUtil("UTILITY_COPYRIGHT");
118
+ out.FOOTER_UTIL_POLICIES = hasUtil("UTILITY_POLICIES");
119
+ out.FOOTER_UTIL_SOCIAL = hasUtil("UTILITY_SOCIAL");
120
+
121
+ // The bar's own on / off is now "is the section there (and shown)".
122
+ const bar = chrome.find((s) => s.type === "ANNOUNCEMENT_BAR");
123
+ out.ANNOUNCEMENT_ENABLED =
124
+ !!bar && (!bar.disabled || !!previewState.value);
125
+ return out;
126
+ });
127
+
128
+ const theme = computed(() => ({
129
+ ...savedTheme.value,
130
+ ...(themeOverlay.value ?? {}),
131
+ ...(groupSettings.value ?? {}),
132
+ }));
133
+
134
+ const savedMenus = computed(() => data.value?.menus ?? {});
135
+
136
+ /** Same overlay treatment. Lets the preview render a menu the merchant
137
+ * created after this page's bootstrap, or edited without saving. */
138
+ const menus = computed(() => {
139
+ const overlay = menusAsMap(menusOverlay.value);
140
+ return overlay ? { ...savedMenus.value, ...overlay } : savedMenus.value;
141
+ });
142
+
143
+ const pages = computed(() => data.value?.pages ?? []);
144
+
145
+ const cartType = computed(() => store.value?.cart_type ?? "DRAWER");
146
+
147
+ const { mediaUrl } = useMediaUrl();
148
+ const storeLogo = computed(() => {
149
+ const s = store.value;
150
+ if (!s) return "";
151
+ return mediaUrl(s.logo?.logo_url || s.logo_url || s.logo || "");
152
+ });
153
+
154
+ const locale = useState("locale", () => "en");
155
+ const dir = computed(() => (RTL_LOCALES.includes(locale.value) ? "rtl" : "ltr"));
156
+
157
+ // ---------------------------------------------------------------------
158
+ // Resolved HERE, in the composable body, while the Nuxt instance is still
159
+ // available. On the server that async context is lost across every await, so
160
+ // anything calling useRequestHeaders() or useState() after one throws:
161
+ //
162
+ // A composable that requires access to the Nuxt instance was called
163
+ // outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function.
164
+ //
165
+ // Refs survive an await fine — it's CREATING one that needs context. So
166
+ // capture the ref now and read .value later.
167
+ // ---------------------------------------------------------------------
168
+ const { init, baseUrl, bootstrapData } = useStoreInit();
169
+
170
+ let requestHost = "";
171
+ if (import.meta.server) {
172
+ const h = useRequestHeaders(["host", "x-forwarded-host"]);
173
+ requestHost = h["x-forwarded-host"] || h.host || "";
174
+ } else if (typeof location !== "undefined") {
175
+ requestHost = location.host;
176
+ }
177
+
178
+ async function load() {
179
+ if (data.value) return data.value;
180
+
181
+ loading.value = true;
182
+ error.value = null;
183
+ try {
184
+ // init() fetches bootstrap directly (no baseUrl needed for that call)
185
+ // and derives baseUrl from the store's username in the response. Reuse
186
+ // that payload instead of firing a second, redundant bootstrap request.
187
+ if (!bootstrapData.value) await init();
188
+ data.value = bootstrapData.value;
189
+
190
+ const available = data.value?.store?.locales ?? ["en"];
191
+ if (!available.includes(locale.value)) {
192
+ locale.value = data.value?.store?.default_locale ?? available[0] ?? "en";
193
+ }
194
+ return data.value;
195
+ } catch (e) {
196
+ // A raw Error must never reach useState — Nuxt serializes every useState
197
+ // into the SSR payload, and an Error is not a plain object. It fails with
198
+ // "Cannot stringify arbitrary non-POJOs", masking the real cause.
199
+ error.value = toPlainError(e);
200
+ throw e;
201
+ } finally {
202
+ loading.value = false;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Sections for one route. Call from a page's useAsyncData; baseUrl and host
208
+ * come from the closure above, so it's safe after an await.
209
+ */
210
+ /**
211
+ * The page payload for a path. In the browser it's cached (utils/requestCache):
212
+ * going back to a page you've seen shows it at once, refreshed in the
213
+ * background when it's over a minute old. The server always fetches fresh.
214
+ */
215
+ async function render(path) {
216
+ if (!baseUrl.value) await init();
217
+ const args = { baseUrl: baseUrl.value, host: requestHost, path };
218
+ return cachedRequest(`render:${args.baseUrl}:${path}`, () => storefrontService.render(args));
219
+ }
220
+
221
+ /** A menu by handle, with a safe empty shape so layouts never have to guard. */
222
+ function menu(handle) {
223
+ return menus.value[handle] ?? { handle, title: "", items: [] };
224
+ }
225
+
226
+ function setLocale(next) {
227
+ const available = store.value?.locales ?? ["en"];
228
+ if (available.includes(next)) locale.value = next;
229
+ }
230
+
231
+ return {
232
+ data, loading, error,
233
+ store, theme, menus, pages, sectionGroups,
234
+ cartType, storeLogo,
235
+ locale, dir,
236
+ baseUrl, requestHost,
237
+ load, render, menu, setLocale,
238
+ };
239
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Read a localized setting.
3
+ *
4
+ * Any schema field marked `localized` is stored as { en: "…", ar: "…" }, not a
5
+ * string. Rendering the object directly prints [object Object] on the page, and
6
+ * it happens on whichever field nobody remembered to wrap.
7
+ *
8
+ * Pipe every text setting through this. It's a no-op on plain strings, so
9
+ * there's no cost to being indiscriminate — and the set of localized fields
10
+ * changes whenever the backend schema does.
11
+ */
12
+ export function useT() {
13
+ const { locale, store } = useStorefront();
14
+
15
+ return function t(value) {
16
+ if (value === null || value === undefined) return "";
17
+ if (typeof value !== "object" || Array.isArray(value)) return value;
18
+ const fallback = store.value?.default_locale ?? "en";
19
+ return value[locale.value] ?? value[fallback] ?? "";
20
+ };
21
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * text_style() settings -> an inline style object.
3
+ *
4
+ * The backend's text_style(prefix, ...) helper emits seven fields for one text
5
+ * element: SIZE, SIZE_MOBILE, WEIGHT, COLOR, LINE_HEIGHT, LETTER_SPACING, FONT.
6
+ * Nearly every section schema splices it in one to four times. SiteFooter grew
7
+ * a local copy of this mapping for its two sets; SlideshowSection would have
8
+ * been the second copy, and every section that adds typography after it another.
9
+ *
10
+ * Same shape as useButtonStyle: reads a settings object rather than a section,
11
+ * so it works for a section's settings and for a block's, and takes a prefix so
12
+ * one component can call it once per text element.
13
+ *
14
+ * const headingStyle = useTextStyle(() => s.value, { prefix: "HEADING_TEXT" })
15
+ * <h2 :style="headingStyle">…</h2>
16
+ *
17
+ * Sizes go out as CSS custom properties rather than a font-size, because the
18
+ * mobile size needs a media query and a component can't write one inline. The
19
+ * consumer pairs them with:
20
+ *
21
+ * font-size: var(--osi-fs);
22
+ * @media (max-width: 767px) { font-size: var(--osi-fs-mobile); }
23
+ *
24
+ * COLOR left empty writes nothing, so the element inherits — which is what
25
+ * makes "leave empty to inherit from the colour scheme" true rather than a
26
+ * label on a field that silently forces a colour.
27
+ *
28
+ * @param {object|Ref|(() => object)} settings
29
+ * @param {object} [options]
30
+ * @param {string} [options.prefix='TEXT']
31
+ * @param {object} [options.defaults] size / weight / font / transform
32
+ * @returns {ComputedRef<object>}
33
+ */
34
+ const FONT_WEIGHTS = { REGULAR: 400, MEDIUM: 500, SEMIBOLD: 600, BOLD: 700 };
35
+ const FONT_FAMILIES = { HEADING: "var(--osi-font-heading)", BODY: "var(--osi-font-body)" };
36
+ const TEXT_TRANSFORMS = {
37
+ NONE: "none",
38
+ UPPERCASE: "uppercase",
39
+ LOWERCASE: "lowercase",
40
+ CAPITALIZE: "capitalize",
41
+ };
42
+
43
+ export function useTextStyle(settings, options = {}) {
44
+ const { prefix = "TEXT", defaults = {} } = options;
45
+ const {
46
+ size = 16,
47
+ weight = "REGULAR",
48
+ font = "BODY",
49
+ transform = "NONE",
50
+ lineHeight = 130,
51
+ } = defaults;
52
+
53
+ return computed(() => {
54
+ const s = toValue(settings) ?? {};
55
+ const get = (key) => s[`${prefix}_${key}`];
56
+
57
+ // null and "" are "not set", not 0 — Number(null) is 0, which used to
58
+ // turn an empty mobile size into a 0px font.
59
+ const num = (value, fallback) => {
60
+ if (value === null || value === undefined || value === "") return fallback;
61
+ const n = Number(value);
62
+ return Number.isFinite(n) ? n : fallback;
63
+ };
64
+
65
+ const base = num(get("SIZE"), size);
66
+
67
+ return {
68
+ "--osi-fs": `${base}px`,
69
+ // Falls back to the desktop size, not to a constant: an unset mobile size
70
+ // means "same as desktop", not "16px".
71
+ "--osi-fs-mobile": `${num(get("SIZE_MOBILE"), base)}px`,
72
+ "--osi-fw": FONT_WEIGHTS[get("WEIGHT")] ?? FONT_WEIGHTS[weight] ?? 400,
73
+ "--osi-lh": `${num(get("LINE_HEIGHT"), lineHeight) / 100}`,
74
+ "--osi-ls": `${num(get("LETTER_SPACING"), 0) / 100}em`,
75
+ "--osi-ff": FONT_FAMILIES[get("FONT")] ?? FONT_FAMILIES[font],
76
+ // undefined rather than "inherit" so the property isn't written at all.
77
+ color: get("COLOR") || undefined,
78
+ textTransform:
79
+ TEXT_TRANSFORMS[get("TRANSFORM")] ?? TEXT_TRANSFORMS[transform] ?? "none",
80
+ };
81
+ });
82
+ }
83
+
84
+ export { FONT_WEIGHTS, FONT_FAMILIES, TEXT_TRANSFORMS };
@@ -0,0 +1,34 @@
1
+ import { BUTTON_HOVERS, BUTTON_STYLES, INPUT_STYLES, LABEL_STYLES, pick } from "#osi/theme/controls";
2
+
3
+ /**
4
+ * The theme's Buttons / Inputs enum choices, resolved.
5
+ *
6
+ * Split from useThemeTokens so a field or button can read "which label
7
+ * style is the theme on?" without building the whole token map.
8
+ */
9
+ export function useThemeControls() {
10
+ const { theme } = useStorefront();
11
+
12
+ const controls = computed(() => {
13
+ const t = theme.value ?? {};
14
+ return {
15
+ buttonStyle: pick(t.BUTTON_STYLE, BUTTON_STYLES) ?? "SOLID",
16
+ buttonHover: pick(t.BUTTON_HOVER_EFFECT, BUTTON_HOVERS) ?? "FADE",
17
+ inputStyle: pick(t.INPUT_STYLE, INPUT_STYLES) ?? "OUTLINE",
18
+ labelStyle: pick(t.INPUT_LABEL_STYLE, LABEL_STYLES) ?? "FLOATING",
19
+ showRequiredMark: t.INPUT_SHOW_REQUIRED_MARK !== false,
20
+ };
21
+ });
22
+
23
+ const rootClasses = computed(() => [
24
+ // Theme → Animations
25
+ theme.value?.ANIMATION_REVEAL ? "osi-reveal" : "",
26
+ `osi-hover--${String(theme.value?.ANIMATION_HOVER || "NONE").toLowerCase()}`,
27
+ `osi-btns--${controls.value.buttonStyle.toLowerCase()}`,
28
+ `osi-btns-hover--${controls.value.buttonHover.toLowerCase()}`,
29
+ `osi-inputs--${controls.value.inputStyle.toLowerCase()}`,
30
+ `osi-labels--${controls.value.labelStyle.toLowerCase()}`,
31
+ ]);
32
+
33
+ return { controls, rootClasses };
34
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Store-wide head tags from the theme — logo/favicon, SEO and sharing,
3
+ * social handles, custom CSS. Called once, from app.vue.
4
+ *
5
+ * Pages add or override on top: a product sets its own title, description and
6
+ * image; these are the fallbacks every page starts from (and all the home page
7
+ * gets, since it sets nothing itself).
8
+ *
9
+ * Theme settings (Shopify's "Logo and favicon", "Search engine listing",
10
+ * "Social media" and "Custom CSS" groups):
11
+ * FAVICON, LOGO icon; logo is the fallback share image
12
+ * SEO_TITLE, SEO_DESCRIPTION home page title / default description
13
+ * SEO_TITLE_FORMAT PAGE_STORE | PAGE_ONLY | STORE_PAGE
14
+ * SEO_TITLE_SEPARATOR "|" "—" "·" "-"
15
+ * SEO_SHARE_IMAGE default og:image
16
+ * SEO_NO_INDEX keep the whole store out of search engines
17
+ * SOCIAL_X_HANDLE twitter:site
18
+ * CUSTOM_CSS appended to <head>
19
+ */
20
+ /** Osimart's favicon, in public/ — used when the store has none. */
21
+ const OSIMART_FAVICON = "/favicon-32x32.png";
22
+
23
+ export function useThemeSeo() {
24
+ const { store, storeLogo } = useStorefront();
25
+ const brand = useThemeGroup("BRAND");
26
+ const seo = useThemeGroup("SEO");
27
+ const social = useThemeGroup("SOCIAL_MEDIA");
28
+ const custom = useThemeGroup("CUSTOM_CSS");
29
+ const { mediaUrl } = useMediaUrl();
30
+ const origin = useRequestURL().origin;
31
+
32
+ const storeName = computed(() => store.value?.name ?? "");
33
+ // The store's favicon (theme setting, then the store's own); none from the
34
+ // API: Osimart's, from public/ (OSIMART_FAVICON).
35
+ const favicon = computed(
36
+ () => mediaUrl(brand.value.FAVICON ?? null) || mediaUrl(store.value?.favicon ?? null)
37
+ );
38
+ const shareImage = computed(
39
+ () => mediaUrl(seo.value.SEO_SHARE_IMAGE ?? null) || mediaUrl(brand.value.LOGO ?? null) || storeLogo.value || null
40
+ );
41
+ const description = computed(
42
+ () => String(seo.value.SEO_DESCRIPTION || store.value?.description || "").trim() || undefined
43
+ );
44
+
45
+ function formatTitle(page) {
46
+ const name = storeName.value;
47
+ const home = String(seo.value.SEO_TITLE || "").trim() || name;
48
+ if (!page) return home;
49
+ const sep = ` ${seo.value.SEO_TITLE_SEPARATOR || "|"} `;
50
+ switch (String(seo.value.SEO_TITLE_FORMAT || "PAGE_STORE").toUpperCase()) {
51
+ case "PAGE_ONLY":
52
+ return page;
53
+ case "STORE_PAGE":
54
+ return name ? `${name}${sep}${page}` : page;
55
+ default:
56
+ return name && page !== name ? `${page}${sep}${name}` : page;
57
+ }
58
+ }
59
+
60
+ const xHandle = computed(() => {
61
+ const raw = String(social.value.SOCIAL_X_HANDLE || "").trim();
62
+ return raw ? (raw.startsWith("@") ? raw : `@${raw}`) : undefined;
63
+ });
64
+
65
+ useHead(() => ({
66
+ titleTemplate: (title) => formatTitle(title),
67
+ link: favicon.value
68
+ ? [
69
+ { rel: "icon", href: favicon.value, key: "favicon" },
70
+ { rel: "apple-touch-icon", href: favicon.value, key: "apple-touch-icon" },
71
+ ]
72
+ : [],
73
+ meta: [
74
+ { name: "description", content: description.value, key: "description" },
75
+ { property: "og:site_name", content: storeName.value || undefined, key: "og:site_name" },
76
+ { property: "og:type", content: "website", key: "og:type" },
77
+ { property: "og:title", content: formatTitle(null), key: "og:title" },
78
+ { property: "og:description", content: description.value, key: "og:description" },
79
+ { property: "og:image", content: shareImage.value || undefined, key: "og:image" },
80
+ { property: "og:url", content: origin, key: "og:url" },
81
+ { name: "twitter:card", content: shareImage.value ? "summary_large_image" : "summary", key: "twitter:card" },
82
+ { name: "twitter:site", content: xHandle.value, key: "twitter:site" },
83
+ ...(seo.value.SEO_NO_INDEX ? [{ name: "robots", content: "noindex, nofollow", key: "robots" }] : []),
84
+ ],
85
+ style: custom.value.CUSTOM_CSS
86
+ ? [{ key: "osi-theme-custom-css", innerHTML: String(custom.value.CUSTOM_CSS) }]
87
+ : [],
88
+ }));
89
+
90
+ return { formatTitle, favicon, shareImage };
91
+ }