@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,329 @@
1
+ <template>
2
+ <div v-if="fees.length" class="flex flex-col gap-3">
3
+ <div
4
+ class="divide-y divide-gray-200 overflow-hidden rounded-xl border border-gray-300 bg-white"
5
+ >
6
+ <template v-for="fee in fees" :key="fee.id">
7
+ <label
8
+ class="flex items-center gap-3 px-3 py-2.5"
9
+ :class="[
10
+ isChecked(fee) ? 'bg-primaryColor bg-opacity-5' : '',
11
+ fee.is_required ? 'cursor-default' : 'cursor-pointer'
12
+ ]"
13
+ >
14
+ <input
15
+ type="checkbox"
16
+ :checked="isChecked(fee)"
17
+ :disabled="fee.is_required"
18
+ class="h-4 w-4 flex-shrink-0 cursor-pointer appearance-none rounded-full border-2 border-gray-300 transition-colors checked:border-primaryColor checked:bg-primaryColor checked:ring-2 checked:ring-white disabled:opacity-50"
19
+ @change="toggleFee(fee)"
20
+ />
21
+ <img v-if="fee.logo" :src="mediaUrl(fee.logo)" width="22" height="22" :alt="fee.beneficiary || fee.name" />
22
+ <span class="flex-1 text-sm">
23
+ {{ fee.name }}
24
+ <span v-if="fee.is_required" class="ml-1 text-xs font-normal text-gray-500">(included)</span>
25
+ <span v-if="fee.description" class="mt-0.5 block text-xs text-gray-500">{{ fee.description }}</span>
26
+ <span v-if="fee.beneficiary" class="mt-0.5 block text-xs text-gray-400">{{ fee.beneficiary }}</span>
27
+ </span>
28
+ <span v-if="!isCustomerAmount(fee)" class="flex-shrink-0 text-sm font-medium text-primaryColor">
29
+ {{ label(fee) }}
30
+ </span>
31
+ </label>
32
+
33
+ <!-- Amount Form (Revealed when checked & fee is Customer Amount) -->
34
+ <div
35
+ v-if="isChecked(fee) && isCustomerAmount(fee)"
36
+ class="space-y-3 bg-gray-50/50 p-4"
37
+ >
38
+ <span class="text-xs font-semibold text-gray-600 block">Select Donation Amount:</span>
39
+
40
+ <!-- Span Pills -->
41
+ <div class="flex flex-wrap gap-2">
42
+ <span
43
+ v-for="preset in getPresets(fee)"
44
+ :key="preset"
45
+ @click="selectPreset(fee, preset)"
46
+ class="px-3 py-1.5 text-xs font-medium rounded-full border cursor-pointer transition-all select-none"
47
+ :class="selectedMode[fee.id] === 'preset' && Number(selection[fee.id]) === preset
48
+ ? 'bg-primaryColor text-white border-primaryColor shadow-sm'
49
+ : 'bg-white text-gray-700 border-gray-300 hover:border-primaryColor hover:text-primaryColor'"
50
+ >
51
+ {{ currency }}{{ preset }}
52
+ </span>
53
+ <span
54
+ @click="selectOther(fee)"
55
+ class="px-3 py-1.5 text-xs font-medium rounded-full border cursor-pointer transition-all select-none"
56
+ :class="selectedMode[fee.id] === 'other'
57
+ ? 'bg-primaryColor text-white border-primaryColor shadow-sm'
58
+ : 'bg-white text-gray-700 border-gray-300 hover:border-primaryColor hover:text-primaryColor'"
59
+ >
60
+ Other
61
+ </span>
62
+ </div>
63
+
64
+ <!-- Custom Input Field (ONLY displayed if 'Other' is selected) -->
65
+ <div v-if="selectedMode[fee.id] === 'other'" class="pt-1">
66
+ <div class="relative flex items-center">
67
+ <span class="osi-fee-currency" aria-hidden="true">{{ currency }}</span>
68
+ <input
69
+ type="number"
70
+ :min="fee.min_amount || 0"
71
+ :max="fee.max_amount || undefined"
72
+ step="0.5"
73
+ :value="selection[fee.id]"
74
+ @input="setAmount(fee, $event.target.value)"
75
+ @blur="clamp(fee)"
76
+ class="osi-input osi-fee-amount"
77
+ aria-label="Custom amount"
78
+ placeholder="Enter custom amount"
79
+ />
80
+ </div>
81
+ </div>
82
+
83
+ <!-- Error message if validation fails -->
84
+ <p v-if="errors[fee.id]" class="text-xs text-red-600 font-medium">
85
+ {{ errors[fee.id] }}
86
+ </p>
87
+ </div>
88
+ </template>
89
+ </div>
90
+ </div>
91
+ </template>
92
+
93
+ <script>
94
+ export default {
95
+ name: "ExtraFees",
96
+ setup() {
97
+ // Tenant-resolved media host: one Nuxt process serves every store, so the
98
+ // logo host has to come from useMediaUrl(), not a build-time constant.
99
+ const { mediaUrl } = useMediaUrl();
100
+ const { store } = useStorefront();
101
+ const currency = computed(
102
+ () => store.value?.default_currency?.symbol || store.value?.default_currency?.code || "$"
103
+ );
104
+ return { mediaUrl, currency };
105
+ },
106
+ props: {
107
+ fees: { type: Array, default: () => [] },
108
+ subtotal: { type: Number, default: 0 },
109
+ },
110
+ emits: ["change"],
111
+ data() {
112
+ return {
113
+ selection: {},
114
+ selectedMode: {}, // { [feeId]: 'preset' | 'other' }
115
+ errors: {},
116
+ };
117
+ },
118
+ methods: {
119
+ isCustomerAmount(fee) {
120
+ if (!fee) return true;
121
+ const calc = String(fee.calculation || fee.calculation_type || fee.type || "").toLowerCase().replace(/_/g, " ");
122
+ if (calc.includes("percentage") || calc.includes("fixed")) return false;
123
+ return true;
124
+ },
125
+
126
+ isChecked(fee) {
127
+ return Object.prototype.hasOwnProperty.call(this.selection, fee.id);
128
+ },
129
+
130
+ toggleFee(fee) {
131
+ if (fee.is_required) return;
132
+ if (this.isChecked(fee)) {
133
+ const newSelection = { ...this.selection };
134
+ delete newSelection[fee.id];
135
+ this.selection = newSelection;
136
+
137
+ const newMode = { ...this.selectedMode };
138
+ delete newMode[fee.id];
139
+ this.selectedMode = newMode;
140
+
141
+ const newErrors = { ...this.errors };
142
+ delete newErrors[fee.id];
143
+ this.errors = newErrors;
144
+ } else {
145
+ if (this.isCustomerAmount(fee)) {
146
+ const presets = this.getPresets(fee);
147
+ const defaultPreset = presets.length ? presets[0] : (Number(fee.min_amount) || 5);
148
+ this.selection = { ...this.selection, [fee.id]: defaultPreset };
149
+ this.selectedMode = { ...this.selectedMode, [fee.id]: "preset" };
150
+ } else {
151
+ this.selection = { ...this.selection, [fee.id]: null };
152
+ }
153
+ }
154
+ this.emitChange();
155
+ },
156
+
157
+ getPresets(fee) {
158
+ const min = Number(fee.min_amount) || 1;
159
+ const max = Number(fee.max_amount) || 1000;
160
+ const candidates = [1,5,10,25,50];
161
+ const presets = candidates.filter(v => v >= min && v <= max);
162
+ if (presets.length < 3) {
163
+ const generated = [];
164
+ let val = Math.max(min, 1);
165
+ while (generated.length < 4 && val <= max) {
166
+ generated.push(val);
167
+ val = val * 2;
168
+ }
169
+ return generated;
170
+ }
171
+ return presets;
172
+ },
173
+
174
+ selectPreset(fee, preset) {
175
+ this.selectedMode = { ...this.selectedMode, [fee.id]: "preset" };
176
+ this.selection = { ...this.selection, [fee.id]: preset };
177
+ const newErrors = { ...this.errors };
178
+ delete newErrors[fee.id];
179
+ this.errors = newErrors;
180
+ this.emitChange();
181
+ },
182
+
183
+ selectOther(fee) {
184
+ this.selectedMode = { ...this.selectedMode, [fee.id]: "other" };
185
+ this.selection = { ...this.selection, [fee.id]: null };
186
+ this.validate(fee);
187
+ this.emitChange();
188
+ },
189
+
190
+ setAmount(fee, raw) {
191
+ const val = raw === "" ? null : Number(raw);
192
+ this.selection = { ...this.selection, [fee.id]: val };
193
+ this.validate(fee);
194
+ this.emitChange();
195
+ },
196
+
197
+ clamp(fee) {
198
+ const raw = this.selection[fee.id];
199
+
200
+ // Leave it empty — don't force a default value in
201
+ if (raw === null || raw === "" || raw === undefined) {
202
+ this.validate(fee);
203
+ this.emitChange();
204
+ return;
205
+ }
206
+
207
+ let v = Number(raw);
208
+ if (fee.min_amount != null) v = Math.max(v, Number(fee.min_amount));
209
+ if (fee.max_amount != null) v = Math.min(v, Number(fee.max_amount));
210
+ this.selection = { ...this.selection, [fee.id]: v };
211
+ this.validate(fee);
212
+ this.emitChange();
213
+ },
214
+
215
+ validate(fee) {
216
+ const v = Number(this.selection[fee.id]);
217
+ if (!this.isCustomerAmount(fee)) return true;
218
+
219
+ const raw = this.selection[fee.id];
220
+ const isEmpty = raw === null || raw === "" || raw === undefined;
221
+
222
+ if (isEmpty) {
223
+ // No error message shown for an empty "Other" field — just mark invalid silently
224
+ const newErrors = { ...this.errors };
225
+ delete newErrors[fee.id];
226
+ this.errors = newErrors;
227
+ return false;
228
+ }
229
+
230
+ if (!v || v <= 0) {
231
+ const newErrors = { ...this.errors };
232
+ delete newErrors[fee.id];
233
+ this.errors = newErrors;
234
+ return false;
235
+ }
236
+
237
+ if (fee.min_amount != null && v < Number(fee.min_amount)) {
238
+ this.errors = { ...this.errors, [fee.id]: `Minimum is $${Number(fee.min_amount)}.` };
239
+ return false;
240
+ }
241
+ if (fee.max_amount != null && v > Number(fee.max_amount)) {
242
+ this.errors = { ...this.errors, [fee.id]: `Maximum is $${Number(fee.max_amount)}.` };
243
+ return false;
244
+ }
245
+ const newErrors = { ...this.errors };
246
+ delete newErrors[fee.id];
247
+ this.errors = newErrors;
248
+ return true;
249
+ },
250
+
251
+ amountFor(fee) {
252
+ if (!this.isChecked(fee)) return 0;
253
+ let amount = 0;
254
+ const calc = String(fee.calculation || fee.calculation_type || fee.type || "").toLowerCase();
255
+ if (calc.includes("percentage")) {
256
+ amount = (this.subtotal * Number(fee.value || 0)) / 100;
257
+ } else if (this.isCustomerAmount(fee)) {
258
+ amount = Number(this.selection[fee.id] || 0);
259
+ } else {
260
+ amount = Number(fee.value || 0);
261
+ }
262
+ if (fee.min_amount != null) amount = Math.max(amount, Number(fee.min_amount));
263
+ if (fee.max_amount != null) amount = Math.min(amount, Number(fee.max_amount));
264
+ return Math.round(Math.max(amount, 0) * 100) / 100;
265
+ },
266
+
267
+ label(fee) {
268
+ if (this.isCustomerAmount(fee)) {
269
+ return this.isChecked(fee) ? `${this.currency}${this.amountFor(fee).toFixed(2)}` : "";
270
+ }
271
+ const calc = String(fee.calculation || fee.calculation_type || fee.type || "").toLowerCase();
272
+ if (calc.includes("percentage")) {
273
+ return `${Number(fee.value)}%`;
274
+ }
275
+ return `${this.currency}${Number(fee.value || 0).toFixed(2)}`;
276
+ },
277
+
278
+ emitChange() {
279
+ const payload = Object.entries(this.selection).map(([id, amount]) => ({
280
+ extra_fee_id: id,
281
+ customer_amount: amount,
282
+ }));
283
+ const total = this.fees.reduce((sum, f) => sum + this.amountFor(f), 0);
284
+ const valid = this.fees.every((f) => !this.isChecked(f) || this.validate(f));
285
+ this.$emit("change", {
286
+ payload,
287
+ total: Math.round(total * 100) / 100,
288
+ valid,
289
+ lines: this.fees
290
+ .filter((f) => this.isChecked(f))
291
+ .map((f) => ({ name: f.name, amount: this.amountFor(f) })),
292
+ });
293
+ },
294
+
295
+ seedRequired() {
296
+ this.fees.forEach((f) => {
297
+ if (f.is_required && !this.isChecked(f)) {
298
+ if (this.isCustomerAmount(f)) {
299
+ const presets = this.getPresets(f);
300
+ const defaultPreset = presets.length ? presets[0] : (Number(f.min_amount) || 5);
301
+ this.selection = { ...this.selection, [f.id]: defaultPreset };
302
+ this.selectedMode = { ...this.selectedMode, [f.id]: "preset" };
303
+ } else {
304
+ this.selection = { ...this.selection, [f.id]: null };
305
+ }
306
+ }
307
+ });
308
+ this.emitChange();
309
+ },
310
+ },
311
+ watch: {
312
+ fees: { handler() { this.seedRequired(); }, immediate: true },
313
+ subtotal() { this.emitChange(); },
314
+ },
315
+ };
316
+ </script>
317
+ <style scoped>
318
+ .osi-fee-currency {
319
+ position: absolute;
320
+ inset-inline-start: var(--osi-input-pad-x, 12px);
321
+ z-index: 1;
322
+ font-size: 0.875rem;
323
+ opacity: 0.6;
324
+ pointer-events: none;
325
+ }
326
+ .osi-fee-amount {
327
+ --in-pad-x: calc(var(--osi-input-pad-x, 12px) + 0.75rem);
328
+ }
329
+ </style>