@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.
- package/README.md +80 -2
- package/THEME_DEV_API.md +80 -0
- package/app/app.vue +154 -0
- package/app/assets/css/osi-controls.css +824 -0
- package/app/components/SectionRenderer.vue +46 -0
- package/app/components/builder/BuilderOverlay.vue +582 -0
- package/app/components/checkout/ExtraFees.vue +329 -0
- package/app/components/checkout/PaymentHandoff.vue +388 -0
- package/app/components/common/Banner.vue +35 -0
- package/app/components/common/ConfirmDeleteModal.vue +117 -0
- package/app/components/common/DropdownSelect.vue +432 -0
- package/app/components/common/Loader.vue +32 -0
- package/app/components/common/OsiField.vue +218 -0
- package/app/components/common/PhoneNumInput.vue +434 -0
- package/app/composables/UseBodyScrollLock.js +122 -0
- package/app/composables/UseThemeGroup.js +575 -0
- package/app/composables/useAddToCart.js +87 -0
- package/app/composables/useAnalytics.js +15 -0
- package/app/composables/useAnnouncements.js +29 -0
- package/app/composables/useApiErrorToast.js +104 -0
- package/app/composables/useAuth.js +99 -0
- package/app/composables/useBanners.js +25 -0
- package/app/composables/useBuilderPreview.js +320 -0
- package/app/composables/useButtonStyle.js +107 -0
- package/app/composables/useCarousel.js +160 -0
- package/app/composables/useCart.js +109 -0
- package/app/composables/useCheckout.js +76 -0
- package/app/composables/useClassificationItems.js +269 -0
- package/app/composables/useClassificationPage.js +545 -0
- package/app/composables/useClassificationUrl.js +63 -0
- package/app/composables/useColorScheme.js +44 -0
- package/app/composables/useContactForm.js +53 -0
- package/app/composables/useContentTree.js +770 -0
- package/app/composables/useDragScroll.js +73 -0
- package/app/composables/useFooterSocials.js +107 -0
- package/app/composables/useInputStyle.js +45 -0
- package/app/composables/useLayoutShell.js +114 -0
- package/app/composables/useLazyVideo.js +36 -0
- package/app/composables/useMediaTypes.js +66 -0
- package/app/composables/useMediaUrl.js +71 -0
- package/app/composables/useMenu.js +100 -0
- package/app/composables/useNavStyle.js +219 -0
- package/app/composables/usePlaceholders.js +294 -0
- package/app/composables/usePolicies.js +26 -0
- package/app/composables/useProductCardMedia.js +135 -0
- package/app/composables/useProductCardStyle.js +132 -0
- package/app/composables/useProductDetail.js +982 -0
- package/app/composables/useProductGallery.js +166 -0
- package/app/composables/useProductItems.js +270 -0
- package/app/composables/useProductUrl.js +60 -0
- package/app/composables/useProductVariants.js +210 -0
- package/app/composables/useQuickShop.js +115 -0
- package/app/composables/useRelatedProducts.js +186 -0
- package/app/composables/useRenderContext.js +45 -0
- package/app/composables/useScopedCss.js +106 -0
- package/app/composables/useSearch.js +35 -0
- package/app/composables/useSectionData.js +462 -0
- package/app/composables/useSectionShell.js +113 -0
- package/app/composables/useSidebar.js +69 -0
- package/app/composables/useStoreData.js +57 -0
- package/app/composables/useStoreInit.js +52 -0
- package/app/composables/useStorefront.js +239 -0
- package/app/composables/useT.js +21 -0
- package/app/composables/useTextStyles.js +84 -0
- package/app/composables/useThemeControls.js +34 -0
- package/app/composables/useThemeSeo.js +91 -0
- package/app/composables/useThemeTokens.js +171 -0
- package/app/composables/useWishlist.js +66 -0
- package/app/logic/components/AnnouncementBar.js +118 -0
- package/app/logic/components/CardGrid.js +125 -0
- package/app/logic/components/CardList.js +98 -0
- package/app/logic/components/CartDrawer.js +273 -0
- package/app/logic/components/CartItem.js +31 -0
- package/app/logic/components/ClassificationBento.js +6 -0
- package/app/logic/components/ClassificationCard.js +6 -0
- package/app/logic/components/ClassificationCarousel.js +115 -0
- package/app/logic/components/ClassificationEditorial.js +41 -0
- package/app/logic/components/ClassificationFeatured.js +57 -0
- package/app/logic/components/ClassificationGrid.js +6 -0
- package/app/logic/components/ClassificationNav.js +61 -0
- package/app/logic/components/ClassificationPart.js +72 -0
- package/app/logic/components/FooterBlock.js +91 -0
- package/app/logic/components/FooterSocials.js +60 -0
- package/app/logic/components/HeaderDropdownList.js +29 -0
- package/app/logic/components/HeaderIcon.js +68 -0
- package/app/logic/components/HeaderLogo.js +39 -0
- package/app/logic/components/HeaderNav.js +268 -0
- package/app/logic/components/MobileMenu.js +280 -0
- package/app/logic/components/ProductBuyButtons.js +66 -0
- package/app/logic/components/ProductCard.js +160 -0
- package/app/logic/components/ProductFilters.js +37 -0
- package/app/logic/components/ProductGallery.js +19 -0
- package/app/logic/components/ProductInfoBlock.js +411 -0
- package/app/logic/components/ProductLightbox.js +12 -0
- package/app/logic/components/ProductQuantity.js +31 -0
- package/app/logic/components/ProductVariantPicker.js +125 -0
- package/app/logic/components/QuickBuy.js +194 -0
- package/app/logic/components/SearchOverlay.js +41 -0
- package/app/logic/components/SidebarMenuItem.js +39 -0
- package/app/logic/components/SiteFooter.js +476 -0
- package/app/logic/components/SiteHeader.js +266 -0
- package/app/logic/components/WhatsAppButton.js +75 -0
- package/app/logic/layouts/default.js +66 -0
- package/app/logic/sections/AnnouncementBarSection.js +6 -0
- package/app/logic/sections/BlockCarousel.js +103 -0
- package/app/logic/sections/ClassificationListSection.js +40 -0
- package/app/logic/sections/ContactFormSection.js +294 -0
- package/app/logic/sections/ContactInfoSection.js +419 -0
- package/app/logic/sections/ContentNode.js +189 -0
- package/app/logic/sections/CountdownSection.js +254 -0
- package/app/logic/sections/CustomHtmlSection.js +51 -0
- package/app/logic/sections/DividerSection.js +31 -0
- package/app/logic/sections/FaqSection.js +53 -0
- package/app/logic/sections/FeatureListSection.js +6 -0
- package/app/logic/sections/FeaturedProductSection.js +40 -0
- package/app/logic/sections/FooterSection.js +16 -0
- package/app/logic/sections/FooterUtilitiesSection.js +24 -0
- package/app/logic/sections/GallerySection.js +232 -0
- package/app/logic/sections/HeaderSection.js +24 -0
- package/app/logic/sections/HeroSection.js +24 -0
- package/app/logic/sections/LogoListSection.js +6 -0
- package/app/logic/sections/MainClassificationSection.js +473 -0
- package/app/logic/sections/MainProductSection.js +211 -0
- package/app/logic/sections/MainSearchSection.js +6 -0
- package/app/logic/sections/MapSection.js +92 -0
- package/app/logic/sections/MediaContentSection.js +38 -0
- package/app/logic/sections/MediaWithTextSection.js +238 -0
- package/app/logic/sections/NewsletterSection.js +126 -0
- package/app/logic/sections/ProductHighlightSection.js +201 -0
- package/app/logic/sections/ProductListSection.js +45 -0
- package/app/logic/sections/ProductRecommendationsSection.js +113 -0
- package/app/logic/sections/RichTextSection.js +6 -0
- package/app/logic/sections/ScrollingTextSection.js +69 -0
- package/app/logic/sections/SearchHeaderSection.js +22 -0
- package/app/logic/sections/SlideshowSection.js +118 -0
- package/app/logic/sections/SpacerSection.js +37 -0
- package/app/logic/sections/SplitShowcaseSection.js +6 -0
- package/app/logic/sections/TestimonialListSection.js +6 -0
- package/app/logic/sections/VideoSection.js +235 -0
- package/app/pages/[category]/[product].vue +196 -0
- package/app/pages/__builder-preview.vue +38 -0
- package/app/pages/categories/[slug].vue +208 -0
- package/app/pages/checkout.vue +1972 -0
- package/app/pages/forgot-password.vue +115 -0
- package/app/pages/index.vue +64 -0
- package/app/pages/login.vue +585 -0
- package/app/pages/order-detail.vue +442 -0
- package/app/pages/pages/[handle].vue +58 -0
- package/app/pages/privacy-policy.vue +46 -0
- package/app/pages/resetpassword.vue +138 -0
- package/app/pages/resetverificationcode.vue +163 -0
- package/app/pages/result.vue +203 -0
- package/app/pages/return-policy.vue +50 -0
- package/app/pages/search.vue +100 -0
- package/app/pages/shipping-policy.vue +46 -0
- package/app/pages/user-profile/components/AddAddressModal.vue +452 -0
- package/app/pages/user-profile/components/Whishlist.vue +137 -0
- package/app/pages/user-profile/components/left-bar.vue +233 -0
- package/app/pages/user-profile/components/orders.vue +162 -0
- package/app/pages/user-profile/components/profile-details.vue +566 -0
- package/app/pages/user-profile/components/settings.vue +194 -0
- package/app/pages/user-profile/index.vue +252 -0
- package/app/pages/verificationcode.vue +188 -0
- package/app/plugins/analytics.client.js +99 -0
- package/app/plugins/geo.js +24 -0
- package/app/plugins/reveal.client.js +53 -0
- package/app/plugins/toast.js +10 -0
- package/app/plugins/vue-query.js +5 -0
- package/app/services/ApiServices.js +74 -0
- package/app/services/AuthBus.js +162 -0
- package/app/services/Authentication/AuthenticationService.js +133 -0
- package/app/services/Authentication/TokenService.js +62 -0
- package/app/services/CartServices.js +91 -0
- package/app/services/CartToken.js +26 -0
- package/app/services/CategoryServices.js +54 -0
- package/app/services/CheckoutServices.js +113 -0
- package/app/services/HomeServices.js +133 -0
- package/app/services/NewsletterServices.js +29 -0
- package/app/services/PaymentServices.js +71 -0
- package/app/services/PoliciesServices.js +16 -0
- package/app/services/ProductServices.js +79 -0
- package/app/services/SearchServices.js +18 -0
- package/app/services/UserServices.js +105 -0
- package/app/services/UtilsServices.js +46 -0
- package/app/services/WishlistServices.js +51 -0
- package/app/services/storefront.service.js +57 -0
- package/app/stores/userStore.js +13 -0
- package/app/theme/categoryTree.js +68 -0
- package/app/theme/controls.js +88 -0
- package/app/theme/registry.js +56 -0
- package/app/utils/analytics.js +864 -0
- package/app/utils/device.js +50 -0
- package/app/utils/eventBus.js +24 -0
- package/app/utils/icon-presets.js +24 -0
- package/app/utils/productOffers.js +118 -0
- package/app/utils/requestCache.js +36 -0
- package/app/utils/scrollToError.js +59 -0
- package/app/utils/toPlainError.js +30 -0
- package/bin/osimart-theme.mjs +282 -0
- package/nuxt/osi-sections.ts +56 -0
- package/nuxt.config.ts +60 -0
- package/package.json +44 -4
- package/public/css/input.css +29 -0
- package/public/css/output.css +2716 -0
- package/public/favicon-32x32.png +0 -0
- package/public/robots.txt +2 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- `theme` variant: two .osi-input controls joined into one box, styled
|
|
3
|
+
entirely by the theme's Inputs group (assets/css/osi-controls.css).
|
|
4
|
+
Used inside <OsiField> on login, register, checkout and the account
|
|
5
|
+
pages. The other variants below are unchanged. -->
|
|
6
|
+
<div
|
|
7
|
+
v-if="effectiveVariant === 'theme'"
|
|
8
|
+
class="osi-phone"
|
|
9
|
+
:class="{ 'is-open': isFocused }"
|
|
10
|
+
>
|
|
11
|
+
<DropdownSelect
|
|
12
|
+
v-model="selectedCode"
|
|
13
|
+
:options="countryOptions"
|
|
14
|
+
:searchable="true"
|
|
15
|
+
:local-search="true"
|
|
16
|
+
smallText
|
|
17
|
+
@search="onSearch"
|
|
18
|
+
@update:modelValue="onCountryChange"
|
|
19
|
+
@open="isFocused = true"
|
|
20
|
+
@close="isFocused = false"
|
|
21
|
+
:fullWidth="false"
|
|
22
|
+
class="osi-phone-dropdown"
|
|
23
|
+
>
|
|
24
|
+
<template #trigger="{ toggle }">
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
class="osi-phone-code"
|
|
28
|
+
:class="[inputClass, { 'is-invalid': !!error }]"
|
|
29
|
+
:aria-label="`Country code ${selectedCountry?.dial || ''}`"
|
|
30
|
+
:disabled="disabled"
|
|
31
|
+
@click="toggle"
|
|
32
|
+
>
|
|
33
|
+
<img
|
|
34
|
+
:key="selectedCountry?.code"
|
|
35
|
+
:src="getFlagUrl(selectedCountry?.code)"
|
|
36
|
+
:alt="selectedCountry?.name"
|
|
37
|
+
/>
|
|
38
|
+
<span v-if="!ispopup">{{ selectedCountry?.dial }}</span>
|
|
39
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
40
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m6 9 6 6 6-6" />
|
|
41
|
+
</svg>
|
|
42
|
+
</button>
|
|
43
|
+
</template>
|
|
44
|
+
</DropdownSelect>
|
|
45
|
+
|
|
46
|
+
<input
|
|
47
|
+
:id="inputId || undefined"
|
|
48
|
+
v-model="phoneNumber"
|
|
49
|
+
type="tel"
|
|
50
|
+
inputmode="numeric"
|
|
51
|
+
autocomplete="tel-national"
|
|
52
|
+
class="osi-phone-number"
|
|
53
|
+
:class="inputClass"
|
|
54
|
+
:placeholder="selectedCountry?.format"
|
|
55
|
+
:disabled="disabled"
|
|
56
|
+
:aria-invalid="!!error"
|
|
57
|
+
@input="onPhoneInput"
|
|
58
|
+
@focus="isFocused = true"
|
|
59
|
+
@blur="isFocused = false"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div
|
|
64
|
+
v-else
|
|
65
|
+
class="w-full"
|
|
66
|
+
:class="[
|
|
67
|
+
effectiveVariant === 'underline' ? 'font-thoren relative' : 'font-poppins'
|
|
68
|
+
]"
|
|
69
|
+
>
|
|
70
|
+
<label
|
|
71
|
+
v-if="effectiveVariant === 'underline'"
|
|
72
|
+
class="underline-label"
|
|
73
|
+
>{{ label }}</label>
|
|
74
|
+
|
|
75
|
+
<div
|
|
76
|
+
class="flex flex-row items-stretch w-full transition-all duration-300"
|
|
77
|
+
:class="[
|
|
78
|
+
effectiveVariant === 'register' ? 'rounded-none bg-transparent' : '',
|
|
79
|
+
effectiveVariant === 'register' && isFocused ? 'border-black' : '',
|
|
80
|
+
effectiveVariant === 'underline' ? 'border-b pt-[3px] border-black min-h-[45.98px]' : '',
|
|
81
|
+
effectiveVariant === 'bare' ? 'bg-transparent' : '',
|
|
82
|
+
]"
|
|
83
|
+
>
|
|
84
|
+
<div
|
|
85
|
+
class="flex flex-col"
|
|
86
|
+
:class="effectiveVariant === 'bare' ? 'border-r border-gray-200 pr-3 justify-center py-2' : 'contents'"
|
|
87
|
+
>
|
|
88
|
+
<label
|
|
89
|
+
v-if="effectiveVariant === 'bare'"
|
|
90
|
+
class="block text-[11px] text-gray-400"
|
|
91
|
+
>{{ label }}</label>
|
|
92
|
+
|
|
93
|
+
<DropdownSelect
|
|
94
|
+
v-model="selectedCode"
|
|
95
|
+
:options="countryOptions"
|
|
96
|
+
:searchable="true"
|
|
97
|
+
:local-search="true"
|
|
98
|
+
smallText
|
|
99
|
+
@search="onSearch"
|
|
100
|
+
@update:modelValue="onCountryChange"
|
|
101
|
+
@open="isFocused = true"
|
|
102
|
+
@close="isFocused = false"
|
|
103
|
+
:fullWidth="false"
|
|
104
|
+
class="shrink min-w-[90px]"
|
|
105
|
+
:class="ispopup ? 'lg:min-w-[70px]':'lg:min-w-[140px]'"
|
|
106
|
+
>
|
|
107
|
+
<template #trigger="{ toggle }">
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
@click="toggle"
|
|
111
|
+
class="dropdown-trigger-btn rounded-l-none w-full text-left text-gray-900 focus:outline-none flex items-center justify-between transition-all duration-300"
|
|
112
|
+
:class="[
|
|
113
|
+
effectiveVariant === 'register'
|
|
114
|
+
? 'bg-transparent rounded-none pl-3 pr-2 border-y border-l border-r py-[9px] text-[1rem]'
|
|
115
|
+
: effectiveVariant === 'underline'
|
|
116
|
+
? 'bg-transparent pl-0 pr-2 py-2'
|
|
117
|
+
: effectiveVariant === 'bare'
|
|
118
|
+
? 'bg-transparent rounded-none border-0 p-0 text-sm'
|
|
119
|
+
: 'bg-white min-h-[34px] rounded-l-md px-2 border py-1',
|
|
120
|
+
effectiveVariant === 'default'
|
|
121
|
+
? (error ? 'border-red-500' : 'border-gray-300')
|
|
122
|
+
: '',
|
|
123
|
+
effectiveVariant === 'register'
|
|
124
|
+
? (error ? 'border-red-500' : 'border-gray-300')
|
|
125
|
+
: '',
|
|
126
|
+
]"
|
|
127
|
+
>
|
|
128
|
+
<div class="flex items-center gap-1.5">
|
|
129
|
+
<span class="text-base pointer-events-none select-none">
|
|
130
|
+
<img
|
|
131
|
+
:key="selectedCountry?.code"
|
|
132
|
+
:src="getFlagUrl(selectedCountry?.code)"
|
|
133
|
+
:alt="selectedCountry?.name"
|
|
134
|
+
class="w-5 h-5 object-contain"
|
|
135
|
+
/>
|
|
136
|
+
</span>
|
|
137
|
+
<span
|
|
138
|
+
class="font-medium pointer-events-none whitespace-nowrap"
|
|
139
|
+
:class="[
|
|
140
|
+
ispopup ? 'hidden':'block',
|
|
141
|
+
effectiveVariant === 'bare' ? 'text-sm' : 'text-xs',
|
|
142
|
+
]"
|
|
143
|
+
>{{ selectedCountry?.dial }}</span>
|
|
144
|
+
</div>
|
|
145
|
+
<svg class="w-3.5 h-3.5 text-gray-900 shrink-0 ml-0.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
146
|
+
<path d="M5.70711 16.1359C5.31659 16.5264 5.31659 17.1596 5.70711 17.5501L10.5993 22.4375C11.3805 23.2179 12.6463 23.2176 13.4271 22.4369L18.3174 17.5465C18.708 17.156 18.708 16.5228 18.3174 16.1323C17.9269 15.7418 17.2937 15.7418 16.9032 16.1323L12.7176 20.3179C12.3271 20.7085 11.6939 20.7085 11.3034 20.3179L7.12132 16.1359C6.7308 15.7454 6.09763 15.7454 5.70711 16.1359Z" fill="currentColor"></path>
|
|
147
|
+
<path d="M18.3174 7.88675C18.708 7.49623 18.708 6.86307 18.3174 6.47254L13.4252 1.58509C12.644 0.804698 11.3783 0.805008 10.5975 1.58579L5.70711 6.47615C5.31658 6.86667 5.31658 7.49984 5.70711 7.89036C6.09763 8.28089 6.7308 8.28089 7.12132 7.89036L11.307 3.70472C11.6975 3.31419 12.3307 3.31419 12.7212 3.70472L16.9032 7.88675C17.2937 8.27728 17.9269 8.27728 18.3174 7.88675Z" fill="currentColor"></path>
|
|
148
|
+
</svg>
|
|
149
|
+
</button>
|
|
150
|
+
</template>
|
|
151
|
+
</DropdownSelect>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div class="flex flex-col min-w-0 w-full flex-2"
|
|
155
|
+
:class="effectiveVariant === 'bare' ? 'items-center justify-center':''">
|
|
156
|
+
<input
|
|
157
|
+
v-model="phoneNumber"
|
|
158
|
+
type="tel"
|
|
159
|
+
:placeholder="selectedCountry?.format"
|
|
160
|
+
class="w-full rounded-r-none text-input outline-none transition-all duration-300"
|
|
161
|
+
:class="[
|
|
162
|
+
effectiveVariant === 'register'
|
|
163
|
+
? 'bg-transparent border-y border-r border-l-0 rounded-none text-[1rem] text-gray-900 pr-3 py-[9px] px-2'
|
|
164
|
+
: effectiveVariant === 'underline'
|
|
165
|
+
? 'bg-transparent border-none py-2 px-0 text-sm text-black'
|
|
166
|
+
: effectiveVariant === 'bare'
|
|
167
|
+
? 'bg-transparent border-0 rounded-none p-0 pl-3 text-[16px] text-primaryColor placeholder:text-[#6b7280]'
|
|
168
|
+
: 'bg-input min-h-[34px] border rounded-r-md rounded-l-none py-1 px-2',
|
|
169
|
+
effectiveVariant === 'default'
|
|
170
|
+
? (error ? 'border-red-500' : 'border-gray-300 focus:border-gray-400')
|
|
171
|
+
: '',
|
|
172
|
+
effectiveVariant === 'register'
|
|
173
|
+
? (error ? 'border-red-500' : 'border-gray-300')
|
|
174
|
+
: '',
|
|
175
|
+
]"
|
|
176
|
+
@input="onPhoneInput"
|
|
177
|
+
@focus="isFocused = true"
|
|
178
|
+
@blur="isFocused = false"
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</template>
|
|
184
|
+
<script setup>
|
|
185
|
+
import { ref, computed, watch, onMounted } from 'vue'
|
|
186
|
+
import DropdownSelect from './DropdownSelect.vue'
|
|
187
|
+
import { useUserStore } from '../../stores/userStore'
|
|
188
|
+
const props = defineProps({
|
|
189
|
+
modelValue: { type: String, default: '' },
|
|
190
|
+
error: { type: [Boolean, String], default: false },
|
|
191
|
+
ispopup:{type:Boolean,default:false},
|
|
192
|
+
isRegister:{type:Boolean,default:false},
|
|
193
|
+
variant: { type: String, default: '' }, // '', 'register', 'underline', 'bare', 'theme'
|
|
194
|
+
label: { type: String, default: 'Phone number' },
|
|
195
|
+
// theme variant only
|
|
196
|
+
inputClass: { type: [String, Array, Object], default: 'osi-input' },
|
|
197
|
+
inputId: { type: String, default: '' },
|
|
198
|
+
disabled: { type: Boolean, default: false },
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
const emit = defineEmits(['update:modelValue', 'update', 'clear-error'])
|
|
202
|
+
|
|
203
|
+
const userStore = useUserStore()
|
|
204
|
+
|
|
205
|
+
const effectiveVariant = computed(() => {
|
|
206
|
+
if (props.variant) return props.variant
|
|
207
|
+
return props.isRegister ? 'register' : 'default'
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
const countries = [
|
|
211
|
+
{ name: "Afghanistan", dial: "+93", flag: "🇦🇫", code: "AF", format: "XX XXX XXXX", len: 9 },
|
|
212
|
+
{ name: "Albania", dial: "+355", flag: "🇦🇱", code: "AL", format: "XX XXX XXXX", len: 9 },
|
|
213
|
+
{ name: "Algeria", dial: "+213", flag: "🇩🇿", code: "DZ", format: "XXX XXX XXXX", len: 10 },
|
|
214
|
+
{ name: "Argentina", dial: "+54", flag: "🇦🇷", code: "AR", format: "XXX XXX XXXX", len: 10 },
|
|
215
|
+
{ name: "Armenia", dial: "+374", flag: "🇦🇲", code: "AM", format: "XX XXX XXX", len: 8 },
|
|
216
|
+
{ name: "Australia", dial: "+61", flag: "🇦🇺", code: "AU", format: "XXX XXX XXX", len: 9 },
|
|
217
|
+
{ name: "Austria", dial: "+43", flag: "🇦🇹", code: "AT", format: "XXX XXXXXXX", len: 10 },
|
|
218
|
+
{ name: "Azerbaijan", dial: "+994", flag: "🇦🇿", code: "AZ", format: "XX XXX XXXX", len: 9 },
|
|
219
|
+
{ name: "Bahrain", dial: "+973", flag: "🇧🇭", code: "BH", format: "XXXX XXXX", len: 8 },
|
|
220
|
+
{ name: "Bangladesh", dial: "+880", flag: "🇧🇩", code: "BD", format: "XXXX XXXXXX", len: 10 },
|
|
221
|
+
{ name: "Belgium", dial: "+32", flag: "🇧🇪", code: "BE", format: "XXX XX XX XX", len: 9 },
|
|
222
|
+
{ name: "Brazil", dial: "+55", flag: "🇧🇷", code: "BR", format: "XX XXXXX XXXX", len: 11 },
|
|
223
|
+
{ name: "Canada", dial: "+1", flag: "🇨🇦", code: "CA", format: "XXX XXX XXXX", len: 10 },
|
|
224
|
+
{ name: "Chile", dial: "+56", flag: "🇨🇱", code: "CL", format: "X XXXX XXXX", len: 9 },
|
|
225
|
+
{ name: "China", dial: "+86", flag: "🇨🇳", code: "CN", format: "XXX XXXX XXXX", len: 11 },
|
|
226
|
+
{ name: "Colombia", dial: "+57", flag: "🇨🇴", code: "CO", format: "XXX XXX XXXX", len: 10 },
|
|
227
|
+
{ name: "Croatia", dial: "+385", flag: "🇭🇷", code: "HR", format: "XX XXX XXXX", len: 9 },
|
|
228
|
+
{ name: "Cyprus", dial: "+357", flag: "🇨🇾", code: "CY", format: "XXXX XXXX", len: 8 },
|
|
229
|
+
{ name: "Czech Republic", dial: "+420", flag: "🇨🇿", code: "CZ", format: "XXX XXX XXX", len: 9 },
|
|
230
|
+
{ name: "Denmark", dial: "+45", flag: "🇩🇰", code: "DK", format: "XXXX XXXX", len: 8 },
|
|
231
|
+
{ name: "Egypt", dial: "+20", flag: "🇪🇬", code: "EG", format: "XXX XXX XXXX", len: 10 },
|
|
232
|
+
{ name: "Ethiopia", dial: "+251", flag: "🇪🇹", code: "ET", format: "XX XXX XXXX", len: 9 },
|
|
233
|
+
{ name: "Finland", dial: "+358", flag: "🇫🇮", code: "FI", format: "XXX XXXXXXX", len: 10 },
|
|
234
|
+
{ name: "France", dial: "+33", flag: "🇫🇷", code: "FR", format: "X XX XX XX XX", len: 9 },
|
|
235
|
+
{ name: "Georgia", dial: "+995", flag: "🇬🇪", code: "GE", format: "XXX XXX XXX", len: 9 },
|
|
236
|
+
{ name: "Germany", dial: "+49", flag: "🇩🇪", code: "DE", format: "XXX XXXXXXXX", len: 11 },
|
|
237
|
+
{ name: "Ghana", dial: "+233", flag: "🇬🇭", code: "GH", format: "XXX XXX XXXX", len: 10 },
|
|
238
|
+
{ name: "Greece", dial: "+30", flag: "🇬🇷", code: "GR", format: "XXX XXX XXXX", len: 10 },
|
|
239
|
+
{ name: "Hungary", dial: "+36", flag: "🇭🇺", code: "HU", format: "XX XXX XXXX", len: 9 },
|
|
240
|
+
{ name: "India", dial: "+91", flag: "🇮🇳", code: "IN", format: "XXXXX XXXXX", len: 10 },
|
|
241
|
+
{ name: "Indonesia", dial: "+62", flag: "🇮🇩", code: "ID", format: "XXX XXXX XXXX", len: 11 },
|
|
242
|
+
{ name: "Iran", dial: "+98", flag: "🇮🇷", code: "IR", format: "XXX XXX XXXX", len: 10 },
|
|
243
|
+
{ name: "Iraq", dial: "+964", flag: "🇮🇶", code: "IQ", format: "XXX XXX XXXX", len: 10 },
|
|
244
|
+
{ name: "Ireland", dial: "+353", flag: "🇮🇪", code: "IE", format: "XX XXX XXXX", len: 9 },
|
|
245
|
+
{ name: "Israel", dial: "+972", flag: "🇮🇱", code: "IL", format: "XX XXX XXXX", len: 9 },
|
|
246
|
+
{ name: "Italy", dial: "+39", flag: "🇮🇹", code: "IT", format: "XXX XXX XXXX", len: 10 },
|
|
247
|
+
{ name: "Japan", dial: "+81", flag: "🇯🇵", code: "JP", format: "XXX XXXX XXXX", len: 11 },
|
|
248
|
+
{ name: "Jordan", dial: "+962", flag: "🇯🇴", code: "JO", format: "X XXXX XXXX", len: 9 },
|
|
249
|
+
{ name: "Kazakhstan", dial: "+7", flag: "🇰🇿", code: "KZ", format: "XXX XXX XXXX", len: 10 },
|
|
250
|
+
{ name: "Kenya", dial: "+254", flag: "🇰🇪", code: "KE", format: "XXX XXX XXX", len: 9 },
|
|
251
|
+
{ name: "Kuwait", dial: "+965", flag: "🇰🇼", code: "KW", format: "XXXX XXXX", len: 8 },
|
|
252
|
+
{ name: "Lebanon", dial: "+961", flag: "🇱🇧", code: "LB", format: "XX XXX XXX", len: 8 },
|
|
253
|
+
{ name: "Libya", dial: "+218", flag: "🇱🇾", code: "LY", format: "XX XXX XXXX", len: 9 },
|
|
254
|
+
{ name: "Malaysia", dial: "+60", flag: "🇲🇾", code: "MY", format: "XX XXXX XXXX", len: 10 },
|
|
255
|
+
{ name: "Mexico", dial: "+52", flag: "🇲🇽", code: "MX", format: "XXX XXX XXXX", len: 10 },
|
|
256
|
+
{ name: "Morocco", dial: "+212", flag: "🇲🇦", code: "MA", format: "XXX XXX XXX", len: 9 },
|
|
257
|
+
{ name: "Netherlands", dial: "+31", flag: "🇳🇱", code: "NL", format: "X XX XX XX XX", len: 9 },
|
|
258
|
+
{ name: "New Zealand", dial: "+64", flag: "🇳🇿", code: "NZ", format: "XX XXX XXXX", len: 9 },
|
|
259
|
+
{ name: "Nigeria", dial: "+234", flag: "🇳🇬", code: "NG", format: "XXX XXX XXXX", len: 10 },
|
|
260
|
+
{ name: "Norway", dial: "+47", flag: "🇳🇴", code: "NO", format: "XXX XX XXX", len: 8 },
|
|
261
|
+
{ name: "Oman", dial: "+968", flag: "🇴🇲", code: "OM", format: "XXXX XXXX", len: 8 },
|
|
262
|
+
{ name: "Pakistan", dial: "+92", flag: "🇵🇰", code: "PK", format: "XXX XXX XXXX", len: 10 },
|
|
263
|
+
{ name: "Palestine", dial: "+970", flag: "🇵🇸", code: "PS", format: "XXX XXX XXXX", len: 10 },
|
|
264
|
+
{ name: "Philippines", dial: "+63", flag: "🇵🇭", code: "PH", format: "XXX XXX XXXX", len: 10 },
|
|
265
|
+
{ name: "Poland", dial: "+48", flag: "🇵🇱", code: "PL", format: "XXX XXX XXX", len: 9 },
|
|
266
|
+
{ name: "Portugal", dial: "+351", flag: "🇵🇹", code: "PT", format: "XXX XXX XXX", len: 9 },
|
|
267
|
+
{ name: "Qatar", dial: "+974", flag: "🇶🇦", code: "QA", format: "XXXX XXXX", len: 8 },
|
|
268
|
+
{ name: "Romania", dial: "+40", flag: "🇷🇴", code: "RO", format: "XXX XXX XXX", len: 9 },
|
|
269
|
+
{ name: "Russia", dial: "+7", flag: "🇷🇺", code: "RU", format: "XXX XXX XXXX", len: 10 },
|
|
270
|
+
{ name: "Saudi Arabia", dial: "+966", flag: "🇸🇦", code: "SA", format: "XX XXX XXXX", len: 9 },
|
|
271
|
+
{ name: "Senegal", dial: "+221", flag: "🇸🇳", code: "SN", format: "XX XXX XXXX", len: 9 },
|
|
272
|
+
{ name: "Serbia", dial: "+381", flag: "🇷🇸", code: "RS", format: "XX XXX XXXX", len: 9 },
|
|
273
|
+
{ name: "Singapore", dial: "+65", flag: "🇸🇬", code: "SG", format: "XXXX XXXX", len: 8 },
|
|
274
|
+
{ name: "South Africa", dial: "+27", flag: "🇿🇦", code: "ZA", format: "XX XXX XXXX", len: 9 },
|
|
275
|
+
{ name: "South Korea", dial: "+82", flag: "🇰🇷", code: "KR", format: "XX XXXX XXXX", len: 10 },
|
|
276
|
+
{ name: "Spain", dial: "+34", flag: "🇪🇸", code: "ES", format: "XXX XXX XXX", len: 9 },
|
|
277
|
+
{ name: "Sudan", dial: "+249", flag: "🇸🇩", code: "SD", format: "XX XXX XXXX", len: 9 },
|
|
278
|
+
{ name: "Sweden", dial: "+46", flag: "🇸🇪", code: "SE", format: "XXX XXX XXXX", len: 10 },
|
|
279
|
+
{ name: "Switzerland", dial: "+41", flag: "🇨🇭", code: "CH", format: "XX XXX XXXX", len: 9 },
|
|
280
|
+
{ name: "Syria", dial: "+963", flag: "🇸🇾", code: "SY", format: "XXX XXX XXX", len: 9 },
|
|
281
|
+
{ name: "Taiwan", dial: "+886", flag: "🇹🇼", code: "TW", format: "XXX XXX XXXX", len: 10 },
|
|
282
|
+
{ name: "Thailand", dial: "+66", flag: "🇹🇭", code: "TH", format: "XX XXX XXXX", len: 9 },
|
|
283
|
+
{ name: "Tunisia", dial: "+216", flag: "🇹🇳", code: "TN", format: "XX XXX XXX", len: 8 },
|
|
284
|
+
{ name: "Turkey", dial: "+90", flag: "🇹🇷", code: "TR", format: "XXX XXX XXXX", len: 10 },
|
|
285
|
+
{ name: "UAE", dial: "+971", flag: "🇦🇪", code: "AE", format: "XX XXX XXXX", len: 9 },
|
|
286
|
+
{ name: "Uganda", dial: "+256", flag: "🇺🇬", code: "UG", format: "XXX XXX XXX", len: 9 },
|
|
287
|
+
{ name: "Ukraine", dial: "+380", flag: "🇺🇦", code: "UA", format: "XX XXX XXXX", len: 9 },
|
|
288
|
+
{ name: "United Kingdom", dial: "+44", flag: "🇬🇧", code: "GB", format: "XXXX XXXXXX", len: 10 },
|
|
289
|
+
{ name: "United States", dial: "+1", flag: "🇺🇸", code: "US", format: "XXX XXX XXXX", len: 10 },
|
|
290
|
+
{ name: "Venezuela", dial: "+58", flag: "🇻🇪", code: "VE", format: "XXX XXX XXXX", len: 10 },
|
|
291
|
+
{ name: "Vietnam", dial: "+84", flag: "🇻🇳", code: "VN", format: "XXX XXX XXXX", len: 10 },
|
|
292
|
+
{ name: "Yemen", dial: "+967", flag: "🇾🇪", code: "YE", format: "XXX XXX XXX", len: 9 },
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
const selectedCode = ref('US')
|
|
296
|
+
const phoneNumber = ref('')
|
|
297
|
+
const isFocused = ref(false)
|
|
298
|
+
const searchQuery = ref('')
|
|
299
|
+
|
|
300
|
+
let countryAutoSet = false
|
|
301
|
+
|
|
302
|
+
const syncCountry = () => {
|
|
303
|
+
const code = userStore.clientIpInfo?.country
|
|
304
|
+
if (!code) return
|
|
305
|
+
const upper = code.toUpperCase()
|
|
306
|
+
if (!countryAutoSet && selectedCode.value === 'US') {
|
|
307
|
+
const exists = countries.some(c => c.code === upper)
|
|
308
|
+
if (exists) {
|
|
309
|
+
selectedCode.value = upper
|
|
310
|
+
countryAutoSet = true
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Safely apply the country after the component finishes hydrating
|
|
316
|
+
onMounted(() => {
|
|
317
|
+
syncCountry()
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
watch(() => userStore.clientIpInfo?.country, syncCountry)
|
|
321
|
+
|
|
322
|
+
function onSearch(query) {
|
|
323
|
+
searchQuery.value = query
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const countryOptions = computed(() => {
|
|
327
|
+
const q = searchQuery.value.toLowerCase()
|
|
328
|
+
|
|
329
|
+
return countries
|
|
330
|
+
.filter(c => c.name.toLowerCase().includes(q) || c.dial.includes(q))
|
|
331
|
+
.map(c => ({
|
|
332
|
+
value: c.code,
|
|
333
|
+
label: `${c.name} (${c.dial})`,
|
|
334
|
+
image: getFlagUrl(c.code),
|
|
335
|
+
flag: getFlagUrl(c.code),
|
|
336
|
+
dial: c.dial,
|
|
337
|
+
name: c.name,
|
|
338
|
+
}))
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
const selectedCountry = computed(() =>
|
|
342
|
+
countries.find(c => c.code === selectedCode.value)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
const status = computed(() => {
|
|
346
|
+
if (!phoneNumber.value) return 'idle'
|
|
347
|
+
return phoneNumber.value.length === selectedCountry.value.len ? 'success' : 'error'
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
const hintText = computed(() => {
|
|
351
|
+
if (!phoneNumber.value) return ''
|
|
352
|
+
if (status.value === 'success') return 'Valid number'
|
|
353
|
+
const diff = selectedCountry.value.len - phoneNumber.value.length
|
|
354
|
+
return diff > 0
|
|
355
|
+
? `${diff} digit${diff > 1 ? 's' : ''} short`
|
|
356
|
+
: `${-diff} digit${-diff > 1 ? 's' : ''} too many`
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
function onCountryChange() {
|
|
360
|
+
countryAutoSet = true
|
|
361
|
+
phoneNumber.value = ''
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function onPhoneInput() {
|
|
365
|
+
phoneNumber.value = phoneNumber.value.replace(/\D/g, '')
|
|
366
|
+
emit('clear-error')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function emitUpdate() {
|
|
370
|
+
const e164 = phoneNumber.value ? `${selectedCountry.value.dial}${phoneNumber.value}` : ''
|
|
371
|
+
const isValid = status.value === 'success'
|
|
372
|
+
emit('update:modelValue', e164)
|
|
373
|
+
emit('update', { isValid, e164 })
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
watch([phoneNumber, selectedCode], emitUpdate)
|
|
377
|
+
|
|
378
|
+
watch(() => props.modelValue, (newVal) => {
|
|
379
|
+
if (!newVal) {
|
|
380
|
+
phoneNumber.value = ''
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
const currentE164 = `${selectedCountry.value?.dial || ''}${phoneNumber.value}`
|
|
384
|
+
if (currentE164 === newVal) return
|
|
385
|
+
|
|
386
|
+
// Prefer keeping the currently selected country if it still fits the new value
|
|
387
|
+
if (selectedCountry.value && newVal.startsWith(selectedCountry.value.dial)) {
|
|
388
|
+
phoneNumber.value = newVal.slice(selectedCountry.value.dial.length)
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const matchedCountry = countries.slice().sort((a,b) => b.dial.length - a.dial.length).find(c => newVal.startsWith(c.dial))
|
|
393
|
+
if (matchedCountry) {
|
|
394
|
+
selectedCode.value = matchedCountry.code
|
|
395
|
+
phoneNumber.value = newVal.slice(matchedCountry.dial.length)
|
|
396
|
+
} else {
|
|
397
|
+
phoneNumber.value = newVal.replace(/\D/g, '')
|
|
398
|
+
}
|
|
399
|
+
}, { immediate: true })
|
|
400
|
+
|
|
401
|
+
const getFlagUrl = (code) => {
|
|
402
|
+
if (!code) return '';
|
|
403
|
+
return `https://flagcdn.com/w40/${code.toLowerCase()}.png`;
|
|
404
|
+
};
|
|
405
|
+
</script>
|
|
406
|
+
|
|
407
|
+
<style scoped>
|
|
408
|
+
.osi-phone-dropdown {
|
|
409
|
+
display: flex;
|
|
410
|
+
flex: none;
|
|
411
|
+
}
|
|
412
|
+
.underline-label {
|
|
413
|
+
position: absolute;
|
|
414
|
+
top: -22px;
|
|
415
|
+
left: 0;
|
|
416
|
+
font-size: 0.85rem;
|
|
417
|
+
color: #000;
|
|
418
|
+
pointer-events: none;
|
|
419
|
+
}
|
|
420
|
+
.dropdown-trigger-btn {
|
|
421
|
+
-webkit-appearance: none;
|
|
422
|
+
appearance: none;
|
|
423
|
+
outline: none !important;
|
|
424
|
+
box-shadow: none !important;
|
|
425
|
+
-webkit-tap-highlight-color: transparent;
|
|
426
|
+
}
|
|
427
|
+
.dropdown-trigger-btn:focus,
|
|
428
|
+
.dropdown-trigger-btn:focus-visible,
|
|
429
|
+
.dropdown-trigger-btn:active,
|
|
430
|
+
.dropdown-trigger-btn:hover {
|
|
431
|
+
outline: none !important;
|
|
432
|
+
box-shadow: none !important;
|
|
433
|
+
}
|
|
434
|
+
</style>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ref-counted page scroll lock.
|
|
3
|
+
*
|
|
4
|
+
* `document.body.style.overflow` was being set directly in 27 places across 8
|
|
5
|
+
* files — some also setting documentElement, some not, and all of them writing
|
|
6
|
+
* "auto" on release rather than restoring the original value. Two overlapping
|
|
7
|
+
* owners (cart drawer open over a product lightbox) meant whichever closed
|
|
8
|
+
* first unlocked the page underneath the other.
|
|
9
|
+
*
|
|
10
|
+
* A count fixes that: the page unlocks when the last owner releases, not the
|
|
11
|
+
* first. The original inline values are captured on the 0 -> 1 transition and
|
|
12
|
+
* restored on 1 -> 0, so a theme that sets its own overflow keeps it.
|
|
13
|
+
*
|
|
14
|
+
* Module-level state is correct here even though this app is multi-tenant: it
|
|
15
|
+
* is browser DOM state, and every function no-ops on the server.
|
|
16
|
+
*/
|
|
17
|
+
let lockCount = 0;
|
|
18
|
+
let previous = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* True while the page is locked (and for a frame after, while its scroll
|
|
22
|
+
* position is put back). useLayoutShell ignores scroll events meanwhile: the
|
|
23
|
+
* lock and the restore are not the visitor scrolling, and reading them used
|
|
24
|
+
* to hide the header (a jump counted as "scrolling down") or reset its state.
|
|
25
|
+
* Client-only in practice — never changes during SSR.
|
|
26
|
+
*/
|
|
27
|
+
export const scrollLocked = ref(false);
|
|
28
|
+
|
|
29
|
+
function applyLock() {
|
|
30
|
+
if (!import.meta.client) return;
|
|
31
|
+
|
|
32
|
+
const body = document.body;
|
|
33
|
+
const root = document.documentElement;
|
|
34
|
+
|
|
35
|
+
previous = {
|
|
36
|
+
bodyOverflow: body.style.overflow,
|
|
37
|
+
rootOverflow: root.style.overflow,
|
|
38
|
+
bodyPaddingRight: body.style.paddingRight,
|
|
39
|
+
y: window.scrollY || root.scrollTop || 0,
|
|
40
|
+
};
|
|
41
|
+
scrollLocked.value = true;
|
|
42
|
+
|
|
43
|
+
// The scrollbar disappears with overflow: hidden — pad its width back so
|
|
44
|
+
// the page (and a sticky header) doesn't shift sideways.
|
|
45
|
+
const gutter = window.innerWidth - root.clientWidth;
|
|
46
|
+
if (gutter > 0) body.style.paddingRight = `${gutter}px`;
|
|
47
|
+
|
|
48
|
+
body.style.overflow = "hidden";
|
|
49
|
+
root.style.overflow = "hidden";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function releaseLock() {
|
|
53
|
+
if (!import.meta.client) return;
|
|
54
|
+
|
|
55
|
+
const y = previous?.y ?? null;
|
|
56
|
+
document.body.style.overflow = previous?.bodyOverflow ?? "";
|
|
57
|
+
document.documentElement.style.overflow = previous?.rootOverflow ?? "";
|
|
58
|
+
document.body.style.paddingRight = previous?.bodyPaddingRight ?? "";
|
|
59
|
+
previous = null;
|
|
60
|
+
|
|
61
|
+
// Some browsers (iOS Safari, and any page with scroll-behavior: smooth)
|
|
62
|
+
// move the page when the lock comes off — closing a drawer used to scroll
|
|
63
|
+
// to the top. Put it back where it was, instantly.
|
|
64
|
+
if (y !== null && Math.abs((window.scrollY || 0) - y) > 1) {
|
|
65
|
+
window.scrollTo({ top: y, left: 0, behavior: "instant" });
|
|
66
|
+
}
|
|
67
|
+
requestAnimationFrame(() => {
|
|
68
|
+
scrollLocked.value = false;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {Ref<boolean>|(() => boolean)} [source]
|
|
74
|
+
* Optional. When given, the lock follows it automatically — this covers most
|
|
75
|
+
* call sites (`watch(showFilters)`, `watch(lightboxOpen)`, `watch(isSheetOpen)`)
|
|
76
|
+
* and removes the release-on-unmount bug those watchers all had.
|
|
77
|
+
*/
|
|
78
|
+
export function useBodyScrollLock(source) {
|
|
79
|
+
// Per-caller, so one component unmounting can only release its own locks.
|
|
80
|
+
let held = 0;
|
|
81
|
+
|
|
82
|
+
function lock() {
|
|
83
|
+
if (!import.meta.client) return;
|
|
84
|
+
held += 1;
|
|
85
|
+
lockCount += 1;
|
|
86
|
+
if (lockCount === 1) applyLock();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function unlock() {
|
|
90
|
+
if (!import.meta.client || held === 0) return;
|
|
91
|
+
held -= 1;
|
|
92
|
+
lockCount = Math.max(0, lockCount - 1);
|
|
93
|
+
if (lockCount === 0) releaseLock();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function set(locked) {
|
|
97
|
+
if (locked) {
|
|
98
|
+
if (held === 0) lock();
|
|
99
|
+
} else {
|
|
100
|
+
while (held > 0) unlock();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Release everything this caller holds, however many times it locked. */
|
|
105
|
+
function releaseAll() {
|
|
106
|
+
while (held > 0) unlock();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (source) {
|
|
110
|
+
watch(
|
|
111
|
+
typeof source === "function" ? source : () => unref(source),
|
|
112
|
+
(locked) => set(!!locked),
|
|
113
|
+
{ immediate: true }
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A drawer closed by navigation never runs its own close handler, which is
|
|
118
|
+
// how the old code left the page permanently unscrollable.
|
|
119
|
+
if (getCurrentInstance()) onScopeDispose(releaseAll);
|
|
120
|
+
|
|
121
|
+
return { lock, unlock, set, releaseAll };
|
|
122
|
+
}
|