@osimart/create-storefront 1.0.24 → 1.0.26

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.
@@ -272,6 +272,7 @@
272
272
  <PhoneNumInput
273
273
  v-model="formData.mobile_number"
274
274
  variant="bare"
275
+ :themed-popup="true"
275
276
  :error="phoneHasError"
276
277
  @update="onPhoneUpdate"
277
278
  />
@@ -375,6 +376,7 @@
375
376
  @update:model-value="selectCountry($event); clearFieldError('country')"
376
377
  placeholder="Country"
377
378
  searchable
379
+ themed
378
380
  local-search
379
381
  full-width
380
382
  field="country"
@@ -408,6 +410,7 @@
408
410
  @update:model-value="selectZone($event); clearFieldError('zone')"
409
411
  placeholder="Zone"
410
412
  searchable
413
+ themed
411
414
  local-search
412
415
  full-width
413
416
  field="zone"
@@ -97,16 +97,20 @@
97
97
  <div
98
98
  v-if="open && !disabled"
99
99
  ref="menuRef"
100
- :style="[floatingStyles, menuWidthStyle, { zIndex: dropdownZIndex }]"
101
- class="absolute font-poppins bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-auto dropdown-select-popup"
102
- :class="dropdownWidth ? '' : 'w-max max-w-xs'"
100
+ :style="[floatingStyles, menuWidthStyle, themeVars, { zIndex: dropdownZIndex }]"
101
+ class="absolute font-poppins border rounded-md shadow-lg max-h-60 overflow-auto dropdown-select-popup"
102
+ :class="[
103
+ dropdownWidth ? '' : 'w-max max-w-xs',
104
+ themed ? 'dropdown-select-popup--themed' : 'bg-white border-gray-300',
105
+ ]"
103
106
  @scroll.passive="onScroll"
104
107
  >
105
108
  <!-- Search -->
106
109
  <div v-if="searchable" class="p-2">
107
110
  <input
108
111
  v-model="searchQuery"
109
- class="bg-input w-full border px-2 py-1 rounded text-sm focus:outline-none"
112
+ class="w-full border px-2 py-1 rounded text-sm focus:outline-none"
113
+ :class="themed ? 'dd-search' : 'bg-input'"
110
114
  placeholder="Search..."
111
115
  />
112
116
  </div>
@@ -116,7 +120,8 @@
116
120
  v-for="opt in filteredOptions"
117
121
  :key="opt.value"
118
122
  :style="{ color: opt.color }"
119
- class="px-4 py-2 flex justify-between items-center hover:bg-gray-100 cursor-pointer"
123
+ class="px-4 py-2 flex justify-between items-center cursor-pointer"
124
+ :class="themed ? 'dd-option' : 'hover:bg-gray-100'"
120
125
  @click="toggleOption(opt.value)"
121
126
  >
122
127
  <div class="flex items-center gap-2 min-w-0">
@@ -141,7 +146,7 @@
141
146
  </li>
142
147
  </ul>
143
148
 
144
- <div v-else-if="!loading" class="px-4 py-2 text-fifthosiblue text-center text-sm">
149
+ <div v-else-if="!loading" class="px-4 py-2 text-fifthosiblue text-center text-sm" :class="{ 'dd-empty': themed }">
145
150
  No results
146
151
  </div>
147
152
 
@@ -193,6 +198,11 @@ const props = defineProps({
193
198
  // Explicit width for the dropdown menu (any valid CSS width, e.g. '320px',
194
199
  // '24rem', '100%'). Overrides the default w-max/max-w-xs/minWidth-match behavior.
195
200
  dropdownWidth: { type: String, default: null },
201
+ // Opt-in: the popup takes its colours from the storefront theme variables
202
+ // (the same ones the form fields use) instead of the hardcoded white/gray.
203
+ // The menu is teleported to <body>, so the variables are copied from the
204
+ // trigger's element when it opens. Off = every existing usage is unchanged.
205
+ themed: { type: Boolean, default: false },
196
206
  });
197
207
  const parentModalZIndex = inject("modalZIndex", null);
198
208
  const dropdownZIndex = computed(() =>
@@ -211,6 +221,38 @@ const rootRef = ref(null);
211
221
  const triggerBtnRef = ref(null);
212
222
  const triggerWidth = ref(0);
213
223
 
224
+ // CSS variables the themed popup needs. The teleported menu sits outside the
225
+ // element that defines them, so they're read from the trigger when it opens.
226
+ // Names match osi-controls.css: the Inputs theme group (--osi-input-*) and the
227
+ // colour scheme (--osi-bg / --osi-text / --osi-border).
228
+ const THEME_VARS = [
229
+ "--osi-bg",
230
+ "--osi-text",
231
+ "--osi-border",
232
+ "--osi-input-bg",
233
+ "--osi-input-fg",
234
+ "--osi-input-border-color",
235
+ // The checkout's card colours (set inline on .osi-ck, so only present there).
236
+ "--ck-card-bg",
237
+ "--ck-card-text",
238
+ "--ck-card-border",
239
+ ];
240
+ const themeVars = ref({});
241
+
242
+ function readThemeVars() {
243
+ if (!props.themed || !rootRef.value) {
244
+ themeVars.value = {};
245
+ return;
246
+ }
247
+ const cs = getComputedStyle(rootRef.value);
248
+ const vars = {};
249
+ for (const name of THEME_VARS) {
250
+ const value = cs.getPropertyValue(name).trim();
251
+ if (value) vars[name] = value;
252
+ }
253
+ themeVars.value = vars;
254
+ }
255
+
214
256
  const menuWidthStyle = computed(() => {
215
257
  if (props.dropdownWidth) return { width: props.dropdownWidth };
216
258
  if (!props.autoWidth) return { minWidth: triggerWidth.value + 'px' };
@@ -410,6 +452,7 @@ watch(open, (isOpen) => {
410
452
  if (isOpen) {
411
453
  eventBus.emit("dropdown:opened", instanceId);
412
454
  triggerWidth.value = rootRef.value ? rootRef.value.offsetWidth : 0;
455
+ readThemeVars();
413
456
  document.addEventListener("mousedown", handleDocumentClick, true);
414
457
  emit("open");
415
458
  } else {
@@ -429,4 +472,30 @@ onBeforeUnmount(() => {
429
472
  onUnmounted(() => {
430
473
  eventBus.off("dropdown:opened", onGlobalDropdownOpened);
431
474
  });
432
- </script>
475
+ </script>
476
+
477
+ <style scoped>
478
+ /* Only applied when the `themed` prop is set. Every colour falls back to the
479
+ old hardcoded one, so a themed popup outside the storefront still looks
480
+ the same as before. */
481
+ .dropdown-select-popup--themed {
482
+ background: var(--osi-input-bg, var(--ck-card-bg, var(--osi-bg, #fff)));
483
+ color: var(--osi-input-fg, var(--ck-card-text, var(--osi-text, inherit)));
484
+ border-color: var(--osi-input-border-color, var(--ck-card-border, var(--osi-border, #d1d5db)));
485
+ }
486
+ .dropdown-select-popup--themed .dd-option:hover {
487
+ background: color-mix(in srgb, currentColor 8%, transparent);
488
+ }
489
+ .dropdown-select-popup--themed .dd-search {
490
+ background: transparent;
491
+ color: inherit;
492
+ border-color: color-mix(in srgb, currentColor 20%, transparent);
493
+ }
494
+ .dropdown-select-popup--themed .dd-search::placeholder {
495
+ color: color-mix(in srgb, currentColor 50%, transparent);
496
+ }
497
+ .dropdown-select-popup--themed .dd-empty {
498
+ color: inherit;
499
+ opacity: 0.6;
500
+ }
501
+ </style>
@@ -14,6 +14,7 @@
14
14
  :searchable="true"
15
15
  :local-search="true"
16
16
  smallText
17
+ themed
17
18
  @search="onSearch"
18
19
  @update:modelValue="onCountryChange"
19
20
  @open="isFocused = true"
@@ -64,7 +65,8 @@
64
65
  v-else
65
66
  class="w-full"
66
67
  :class="[
67
- effectiveVariant === 'underline' ? 'font-thoren relative' : 'font-poppins'
68
+ effectiveVariant === 'underline' ? 'font-thoren relative' : 'font-poppins',
69
+ { 'phone-input-themed': themedPopup && effectiveVariant === 'bare' }
68
70
  ]"
69
71
  >
70
72
  <label
@@ -100,6 +102,7 @@
100
102
  @update:modelValue="onCountryChange"
101
103
  @open="isFocused = true"
102
104
  @close="isFocused = false"
105
+ :themed="themedPopup"
103
106
  :fullWidth="false"
104
107
  class="shrink min-w-[90px]"
105
108
  :class="ispopup ? 'lg:min-w-[70px]':'lg:min-w-[140px]'"
@@ -196,6 +199,9 @@ const props = defineProps({
196
199
  inputClass: { type: [String, Array, Object], default: 'osi-input' },
197
200
  inputId: { type: String, default: '' },
198
201
  disabled: { type: Boolean, default: false },
202
+ // Non-theme variants only: opt the country popup into the theme colours.
203
+ // (The `theme` variant is always themed.)
204
+ themedPopup: { type: Boolean, default: false },
199
205
  })
200
206
 
201
207
  const emit = defineEmits(['update:modelValue', 'update', 'clear-error'])
@@ -431,4 +437,20 @@ const getFlagUrl = (code) => {
431
437
  outline: none !important;
432
438
  box-shadow: none !important;
433
439
  }
434
- </style>
440
+
441
+ /* Keep the compact bare layout while using the shared Inputs color tokens. */
442
+ .phone-input-themed .dropdown-trigger-btn,
443
+ .phone-input-themed .text-input {
444
+ color: var(--osi-input-fg, inherit);
445
+ }
446
+ .phone-input-themed .text-input {
447
+ background-color: var(--osi-input-bg, transparent);
448
+ }
449
+ .phone-input-themed .text-input::placeholder {
450
+ color: var(--osi-input-placeholder, currentColor);
451
+ opacity: 1;
452
+ }
453
+ .phone-input-themed .border-r {
454
+ border-color: var(--osi-input-border-color, #e5e7eb);
455
+ }
456
+ </style>
@@ -563,16 +563,39 @@ export function createProductDetail({ product, settings = {}, loading = false }
563
563
  const originalPriceRange = computed(() => pricing.value.original);
564
564
  /** The sale boxes under the price (tiers of 2+ items), each { id, text, reached, applied }. */
565
565
  const discountBadges = computed(() => pricing.value.badges ?? []);
566
+ /**
567
+ * What the customization adds to every unit: per-unit fees plus the price of
568
+ * each add-on picked. The prices shown on the page include it; `pricing`
569
+ * itself (sales, "Save %", hasPrice) stays on the product's own price.
570
+ */
571
+ const extraPerUnit = computed(() =>
572
+ round2((Number(customization.deltaPerUnit.value) || 0) + (Number(customization.addonsPerUnit.value) || 0))
573
+ );
574
+
575
+ /** A price range with the extra added. A bound with no price stays unpriced (never "$5"). */
576
+ function withExtra(range) {
577
+ const extra = extraPerUnit.value;
578
+ if (!range || !(extra > 0)) return range;
579
+ const add = (n) => (Number(n) > 0 ? round2(Number(n) + extra) : n);
580
+ return { min: add(range.min), max: add(range.max) };
581
+ }
582
+
566
583
  /**
567
584
  * The line total for the quantity picked — { quantity, before, after, saving,
568
585
  * text: { before, after, saving } } — or null (1 item, or no single price).
586
+ * Includes the customization extra for every item; the saving is unchanged.
569
587
  */
570
588
  const lineTotal = computed(() => {
571
589
  const t = pricing.value.total;
572
590
  if (!t) return null;
591
+ const extra = round2(extraPerUnit.value * t.quantity);
592
+ const before = round2(t.before + extra);
593
+ const after = round2(t.after + extra);
573
594
  return {
574
595
  ...t,
575
- text: { before: formatMoney(t.before), after: formatMoney(t.after), saving: formatMoney(t.saving) },
596
+ before,
597
+ after,
598
+ text: { before: formatMoney(before), after: formatMoney(after), saving: formatMoney(t.saving) },
576
599
  };
577
600
  });
578
601
  /** The same, as one line (older callers). */
@@ -592,7 +615,7 @@ export function createProductDetail({ product, settings = {}, loading = false }
592
615
  return Math.round(((was - now) / was) * 100);
593
616
  });
594
617
 
595
- const priceText = computed(() => formatRange(priceRange.value));
618
+ const priceText = computed(() => formatRange(withExtra(priceRange.value)));
596
619
 
597
620
  /**
598
621
  * Whether the selection has a real price. A product (or variant) with no
@@ -604,9 +627,9 @@ export function createProductDetail({ product, settings = {}, loading = false }
604
627
  });
605
628
  const comparePriceText = computed(() =>
606
629
  pricing.value.was
607
- ? formatRange(pricing.value.was)
630
+ ? formatRange(withExtra(pricing.value.was))
608
631
  : isOnSale.value
609
- ? formatRange(originalPriceRange.value)
632
+ ? formatRange(withExtra(originalPriceRange.value))
610
633
  : ""
611
634
  );
612
635
 
@@ -954,6 +977,7 @@ export function createProductDetail({ product, settings = {}, loading = false }
954
977
 
955
978
  // price
956
979
  pricing,
980
+ extraPerUnit,
957
981
  priceRange,
958
982
  originalPriceRange,
959
983
  discountMessage,
@@ -1015,4 +1039,4 @@ export function provideProductDetail(context) {
1015
1039
  */
1016
1040
  export function useProductDetail() {
1017
1041
  return inject(PRODUCT_DETAIL_KEY, null);
1018
- }
1042
+ }
@@ -146,6 +146,8 @@ export default function useLogic() {
146
146
  if (s.HEADER_ICON_BORDER) style["--osi-header-icon-border"] = s.HEADER_ICON_BORDER;
147
147
  if (s.HEADER_CART_BADGE_BG) style["--osi-header-badge-bg"] = s.HEADER_CART_BADGE_BG;
148
148
  if (s.HEADER_CART_BADGE_TEXT) style["--osi-header-badge-text"] = s.HEADER_CART_BADGE_TEXT;
149
+ if (s.HEADER_BORDER_COLOR) style["--osi-header-border"] = s.HEADER_BORDER_COLOR;
150
+ if (s.HEADER_BORDER_WIDTH) style["--osi-header-border-w"] = `${s.HEADER_BORDER_WIDTH}px`;
149
151
  style["--osi-header-pad-y"] = `${s.HEADER_PADDING_Y}px`;
150
152
  // Side padding lives on the header itself, not on the inner box — see the
151
153
  // note on .osi-header-inner below. 16 is COMMON_STYLE_SETTINGS' PADDING_X
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osimart/create-storefront",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "Osimart storefront platform — logic, platform pages and CLI; extended by every theme repo",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",