orionplace-nuxt 0.6.0 → 0.6.2

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.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, nextTick, onMounted, ref, watch } from 'vue';
2
+ import { computed, nextTick, onMounted, ref, shallowRef, watch } from 'vue';
3
3
  import { useRuntimeConfig } from '#imports';
4
4
  import { useOrionplaceStore } from '@/stores/orionplace';
5
5
  import { useOrionplaceProductsStore } from '@/stores/orionplaceProducts';
@@ -25,6 +25,13 @@
25
25
  nativeAppOrigin,
26
26
  } from '@/utils/orionplace/nativeApp';
27
27
  import { requestBlogPost } from '@/utils/orionplace/blogLayout';
28
+ import {
29
+ blogPlacementProductIds,
30
+ blogPlacementRequests,
31
+ blogProductPlacements,
32
+ } from '@/utils/orionplace/blogProductPlacements';
33
+ import type { IBlogProductPlacement } from '@/utils/orionplace/blogProductPlacements';
34
+ import type { IProductSearchSeller } from '@/interfaces/orionplace/products/IProduct';
28
35
  import {
29
36
  blogViewerImage,
30
37
  blogViewerImages,
@@ -46,6 +53,7 @@
46
53
  import { NuxtLink } from '#components';
47
54
  import BlogComments from '@/components/orionplace/blog/BlogComments.vue';
48
55
  import ProductGroup from '@/components/orionplace/products/ProductGroup.vue';
56
+ import ProductCard from '@/components/orionplace/products/ProductCard.vue';
49
57
  import '@/components/orionplace/shared/features.css';
50
58
  import OrionplaceEmptyState from '@/components/orionplace/shared/OrionplaceEmptyState.vue';
51
59
 
@@ -95,6 +103,8 @@
95
103
  useRuntimeConfig().public.orionplaceAppOrigin,
96
104
  );
97
105
  const productIds = ref<number[]>([]);
106
+ // Product cards the author placed in the text (see blogProductPlacements).
107
+ const placements = shallowRef<IBlogProductPlacement[]>([]);
98
108
  const wasShown = ref(false);
99
109
  const settled = ref(false);
100
110
  const labels = computed(() => blogLabels(selectionStore.localeCode));
@@ -244,6 +254,48 @@
244
254
  wasShown.value = true;
245
255
  showProducts(props.initialPost.related_products);
246
256
  }
257
+ function placementShown(placement: IBlogProductPlacement) {
258
+ return placement.productIds.some((id) => productStore.getProduct(id, 'products'));
259
+ }
260
+
261
+ // Placed products that no list loaded yet come with one request per 35.
262
+ async function loadPlacementProducts(ids: number[]) {
263
+ const missing = ids.filter((id) => !productStore.getProduct(id, 'products'));
264
+
265
+ for (const chunk of blogPlacementRequests(missing)) {
266
+ try {
267
+ const data = await api.request<{
268
+ products?: { data?: IProduct[] };
269
+ selects?: { sellers?: IProductSearchSeller[] };
270
+ }>('products_search', {
271
+ filters: { id_in: chunk.join(',') },
272
+ paginate: { page: 1, pp_items: chunk.length },
273
+ });
274
+
275
+ for (const product of data?.products?.data ?? []) {
276
+ productStore.setProduct(product, 'products', data?.selects?.sellers ?? []);
277
+ }
278
+ } catch {
279
+ // A placement without its products stays empty and hidden.
280
+ }
281
+ }
282
+ }
283
+
284
+ // New text replaces the placement blocks: the cards leave them first.
285
+ watch(html, () => {
286
+ placements.value = [];
287
+ }, { flush: 'pre' });
288
+ watch(
289
+ [html, mounted, () => post.value?.id],
290
+ () => {
291
+ void nextTick(() => {
292
+ placements.value = mounted.value ? blogProductPlacements(article.value) : [];
293
+ void loadPlacementProducts(blogPlacementProductIds(placements.value));
294
+ });
295
+ },
296
+ { flush: 'post' },
297
+ );
298
+
247
299
  function openableImages() {
248
300
  return blogViewerImages([coverImage.value, article.value]);
249
301
  }
@@ -388,6 +440,52 @@
388
440
  {{ paragraph }}
389
441
  </p>
390
442
  </div>
443
+ <template
444
+ v-for="(placement, index) in placements"
445
+ :key="index"
446
+ >
447
+ <Teleport
448
+ v-if="placementShown(placement)"
449
+ :to="placement.element"
450
+ >
451
+ <ProductCard
452
+ v-if="placement.layout === 'single'"
453
+ :product-id="placement.productIds[0]!"
454
+ :hidden-badges="placement.hiddenBadges"
455
+ :disabled="props.productActionsDisabled"
456
+ data-blog-placement="single"
457
+ @select="emit('selectProduct', $event)"
458
+ @select-seller="emit('selectSeller', $event)"
459
+ @select-variant="emit('selectVariant', $event)"
460
+ @request-secondary-variants="emit('requestSecondaryVariants', $event)"
461
+ @select-secondary-variant="emit('selectSecondaryVariant', $event)"
462
+ @toggle-favorite="emit('toggleFavorite', $event)"
463
+ @add="emit('add', $event)"
464
+ @remove="emit('remove', $event)"
465
+ @subscribe="emit('subscribe', $event)"
466
+ @unsubscribe="emit('unsubscribe', $event)"
467
+ @preorder="emit('preorder', $event)"
468
+ />
469
+ <ProductGroup
470
+ v-else
471
+ :product-ids="placement.productIds"
472
+ :hidden-badges="placement.hiddenBadges"
473
+ :disabled="props.productActionsDisabled"
474
+ data-blog-placement="list"
475
+ @select="emit('selectProduct', $event)"
476
+ @select-seller="emit('selectSeller', $event)"
477
+ @select-variant="emit('selectVariant', $event)"
478
+ @request-secondary-variants="emit('requestSecondaryVariants', $event)"
479
+ @select-secondary-variant="emit('selectSecondaryVariant', $event)"
480
+ @toggle-favorite="emit('toggleFavorite', $event)"
481
+ @add="emit('add', $event)"
482
+ @remove="emit('remove', $event)"
483
+ @subscribe="emit('subscribe', $event)"
484
+ @unsubscribe="emit('unsubscribe', $event)"
485
+ @preorder="emit('preorder', $event)"
486
+ />
487
+ </Teleport>
488
+ </template>
391
489
  <div
392
490
  v-if="!full"
393
491
  class="op-feature-panel op-feature-stack"
@@ -555,143 +653,144 @@
555
653
  clear: both;
556
654
  }
557
655
 
558
- .op-feature-prose :deep(:where(p, ul, ol, blockquote, pre, figure)) {
656
+ /* Product cards placed in the text keep their own look: the text rules skip placement blocks. */
657
+ .op-feature-prose :deep(:where(p, ul, ol, blockquote, pre, figure):where(:not([data-product-id] *, [data-product-ids] *))) {
559
658
  margin: 1rem 0;
560
659
  }
561
660
 
562
661
  /* A new section or wide block starts below a floated image. */
563
- .op-feature-prose :deep(:where(h1, h2, h3, h4, h5, h6, figure, table, pre)) {
662
+ .op-feature-prose :deep(:where(h1, h2, h3, h4, h5, h6, figure, table, pre):where(:not([data-product-id] *, [data-product-ids] *))) {
564
663
  clear: both;
565
664
  }
566
665
 
567
- .op-feature-prose :deep(:where(h1, h2, h3, h4, h5, h6)) {
666
+ .op-feature-prose :deep(:where(h1, h2, h3, h4, h5, h6):where(:not([data-product-id] *, [data-product-ids] *))) {
568
667
  margin: 2rem 0 1rem;
569
668
  font-weight: 500;
570
669
  line-height: 1.3;
571
670
  }
572
671
 
573
- .op-feature-prose :deep(h1) {
672
+ .op-feature-prose :deep(h1:where(:not([data-product-id] *, [data-product-ids] *))) {
574
673
  font-size: 1.875rem;
575
674
  }
576
675
 
577
- .op-feature-prose :deep(h2) {
676
+ .op-feature-prose :deep(h2:where(:not([data-product-id] *, [data-product-ids] *))) {
578
677
  font-size: 1.5rem;
579
678
  }
580
679
 
581
- .op-feature-prose :deep(h3) {
680
+ .op-feature-prose :deep(h3:where(:not([data-product-id] *, [data-product-ids] *))) {
582
681
  font-size: 1.25rem;
583
682
  }
584
683
 
585
- .op-feature-prose :deep(h4) {
684
+ .op-feature-prose :deep(h4:where(:not([data-product-id] *, [data-product-ids] *))) {
586
685
  font-size: 1.125rem;
587
686
  }
588
687
 
589
- .op-feature-prose :deep(h5) {
688
+ .op-feature-prose :deep(h5:where(:not([data-product-id] *, [data-product-ids] *))) {
590
689
  font-size: 1rem;
591
690
  }
592
691
 
593
- .op-feature-prose :deep(h6) {
692
+ .op-feature-prose :deep(h6:where(:not([data-product-id] *, [data-product-ids] *))) {
594
693
  font-size: 0.875rem;
595
694
  text-transform: uppercase;
596
695
  letter-spacing: 0.08em;
597
696
  }
598
697
 
599
- .op-feature-prose :deep(:where(strong, b)) {
698
+ .op-feature-prose :deep(:where(strong, b):where(:not([data-product-id] *, [data-product-ids] *))) {
600
699
  font-weight: 500;
601
700
  }
602
701
 
603
- .op-feature-prose :deep(:where(sub, sup)) {
702
+ .op-feature-prose :deep(:where(sub, sup):where(:not([data-product-id] *, [data-product-ids] *))) {
604
703
  font-size: 0.75em;
605
704
  line-height: 0;
606
705
  }
607
706
 
608
- .op-feature-prose :deep(mark) {
707
+ .op-feature-prose :deep(mark:where(:not([data-product-id] *, [data-product-ids] *))) {
609
708
  color: inherit;
610
709
  background: var(--orionplace-attention-surface);
611
710
  }
612
711
 
613
- .op-feature-prose :deep(a) {
712
+ .op-feature-prose :deep(a:where(:not([data-product-id] *, [data-product-ids] *))) {
614
713
  color: var(--orionplace-text);
615
714
  text-decoration: none;
616
715
  border-bottom: 1px solid currentColor;
617
716
  }
618
717
 
619
- .op-feature-prose :deep(a:not([href])) {
718
+ .op-feature-prose :deep(a:not([href]):where(:not([data-product-id] *, [data-product-ids] *))) {
620
719
  border-bottom: 0;
621
720
  }
622
721
 
623
- .op-feature-prose :deep(a:focus-visible) {
722
+ .op-feature-prose :deep(a:focus-visible:where(:not([data-product-id] *, [data-product-ids] *))) {
624
723
  color: var(--orionplace-accent);
625
724
  outline: none;
626
725
  }
627
726
 
628
727
  @media (hover: hover) {
629
- .op-feature-prose :deep(a:hover) {
728
+ .op-feature-prose :deep(a:hover:where(:not([data-product-id] *, [data-product-ids] *))) {
630
729
  color: var(--orionplace-accent);
631
730
  outline: none;
632
731
  }
633
732
  }
634
733
 
635
- .op-feature-prose :deep(:where(ul, ol)) {
734
+ .op-feature-prose :deep(:where(ul, ol):where(:not([data-product-id] *, [data-product-ids] *))) {
636
735
  padding-left: 1.5rem;
637
736
  }
638
737
 
639
- .op-feature-prose :deep(ul) {
738
+ .op-feature-prose :deep(ul:where(:not([data-product-id] *, [data-product-ids] *))) {
640
739
  list-style: disc;
641
740
  }
642
741
 
643
- .op-feature-prose :deep(ol) {
742
+ .op-feature-prose :deep(ol:where(:not([data-product-id] *, [data-product-ids] *))) {
644
743
  list-style: decimal;
645
744
  }
646
745
 
647
- .op-feature-prose :deep(li > :where(ul, ol)) {
746
+ .op-feature-prose :deep(li > :where(ul, ol):where(:not([data-product-id] *, [data-product-ids] *))) {
648
747
  margin: 0.25rem 0;
649
748
  }
650
749
 
651
- .op-feature-prose :deep(li > p) {
750
+ .op-feature-prose :deep(li > p:where(:not([data-product-id] *, [data-product-ids] *))) {
652
751
  margin: 0;
653
752
  }
654
753
 
655
- .op-feature-prose :deep(ul[data-type='taskList']) {
754
+ .op-feature-prose :deep(ul[data-type='taskList']:where(:not([data-product-id] *, [data-product-ids] *))) {
656
755
  list-style: none;
657
756
  padding-left: 0;
658
757
  }
659
758
 
660
- .op-feature-prose :deep(li[data-type='taskItem']) {
759
+ .op-feature-prose :deep(li[data-type='taskItem']:where(:not([data-product-id] *, [data-product-ids] *))) {
661
760
  display: flex;
662
761
  align-items: flex-start;
663
762
  gap: 0.5rem;
664
763
  }
665
764
 
666
- .op-feature-prose :deep(li[data-type='taskItem'] > label) {
765
+ .op-feature-prose :deep(li[data-type='taskItem'] > label:where(:not([data-product-id] *, [data-product-ids] *))) {
667
766
  display: flex;
668
767
  align-items: center;
669
768
  min-height: 1.85em;
670
769
  }
671
770
 
672
- .op-feature-prose :deep(li[data-type='taskItem'] > div > p) {
771
+ .op-feature-prose :deep(li[data-type='taskItem'] > div > p:where(:not([data-product-id] *, [data-product-ids] *))) {
673
772
  margin: 0;
674
773
  }
675
774
 
676
- .op-feature-prose :deep(blockquote) {
775
+ .op-feature-prose :deep(blockquote:where(:not([data-product-id] *, [data-product-ids] *))) {
677
776
  border-left: 2px solid var(--orionplace-border);
678
777
  padding-left: 1rem;
679
778
  color: var(--orionplace-muted);
680
779
  }
681
780
 
682
- .op-feature-prose :deep(hr) {
781
+ .op-feature-prose :deep(hr:where(:not([data-product-id] *, [data-product-ids] *))) {
683
782
  margin: 2rem 0;
684
783
  border: 0;
685
784
  border-top: 1px solid var(--orionplace-border);
686
785
  }
687
786
 
688
- .op-feature-prose :deep(code) {
787
+ .op-feature-prose :deep(code:where(:not([data-product-id] *, [data-product-ids] *))) {
689
788
  padding: 0.1em 0.3em;
690
789
  font-size: 0.875em;
691
790
  background: var(--orionplace-muted-surface);
692
791
  }
693
792
 
694
- .op-feature-prose :deep(pre) {
793
+ .op-feature-prose :deep(pre:where(:not([data-product-id] *, [data-product-ids] *))) {
695
794
  overflow-x: auto;
696
795
  padding: 1rem;
697
796
  line-height: 1.6;
@@ -699,13 +798,13 @@
699
798
  background: var(--orionplace-muted-surface);
700
799
  }
701
800
 
702
- .op-feature-prose :deep(pre code) {
801
+ .op-feature-prose :deep(pre code:where(:not([data-product-id] *, [data-product-ids] *))) {
703
802
  padding: 0;
704
803
  background: transparent;
705
804
  }
706
805
 
707
806
  /* A wide table scrolls inside the article instead of widening the page. */
708
- .op-feature-prose :deep(table) {
807
+ .op-feature-prose :deep(table:where(:not([data-product-id] *, [data-product-ids] *))) {
709
808
  display: block;
710
809
  max-width: 100%;
711
810
  overflow-x: auto;
@@ -713,7 +812,7 @@
713
812
  border-collapse: collapse;
714
813
  }
715
814
 
716
- .op-feature-prose :deep(:where(th, td)) {
815
+ .op-feature-prose :deep(:where(th, td):where(:not([data-product-id] *, [data-product-ids] *))) {
717
816
  min-width: 6rem;
718
817
  padding: 0.5rem 0.75rem;
719
818
  vertical-align: top;
@@ -721,46 +820,46 @@
721
820
  border: 1px solid var(--orionplace-border);
722
821
  }
723
822
 
724
- .op-feature-prose :deep(th) {
823
+ .op-feature-prose :deep(th:where(:not([data-product-id] *, [data-product-ids] *))) {
725
824
  font-weight: 500;
726
825
  background: var(--orionplace-muted-surface);
727
826
  }
728
827
 
729
- .op-feature-prose :deep(caption) {
828
+ .op-feature-prose :deep(caption:where(:not([data-product-id] *, [data-product-ids] *))) {
730
829
  padding: 0.5rem 0;
731
830
  color: var(--orionplace-muted);
732
831
  text-align: left;
733
832
  }
734
833
 
735
- .op-feature-prose :deep([data-text-align='center']) {
834
+ .op-feature-prose :deep([data-text-align='center']:where(:not([data-product-id] *, [data-product-ids] *))) {
736
835
  text-align: center;
737
836
  }
738
837
 
739
- .op-feature-prose :deep([data-text-align='right']) {
838
+ .op-feature-prose :deep([data-text-align='right']:where(:not([data-product-id] *, [data-product-ids] *))) {
740
839
  text-align: right;
741
840
  }
742
841
 
743
- .op-feature-prose :deep([data-vertical-align='middle']) {
842
+ .op-feature-prose :deep([data-vertical-align='middle']:where(:not([data-product-id] *, [data-product-ids] *))) {
744
843
  vertical-align: middle;
745
844
  }
746
845
 
747
- .op-feature-prose :deep([data-vertical-align='bottom']) {
846
+ .op-feature-prose :deep([data-vertical-align='bottom']:where(:not([data-product-id] *, [data-product-ids] *))) {
748
847
  vertical-align: bottom;
749
848
  }
750
849
 
751
850
  /* Images that open in the viewer. */
752
851
  [data-blog-article-cover][data-blog-image-viewer],
753
- .op-feature-prose :deep(img[data-blog-image-viewer]) {
852
+ .op-feature-prose :deep(img[data-blog-image-viewer]:where(:not([data-product-id] *, [data-product-ids] *))) {
754
853
  cursor: zoom-in;
755
854
  }
756
855
 
757
856
  [data-blog-article-cover][data-blog-image-viewer]:focus-visible,
758
- .op-feature-prose :deep(img[data-blog-image-viewer]:focus-visible) {
857
+ .op-feature-prose :deep(img[data-blog-image-viewer]:focus-visible:where(:not([data-product-id] *, [data-product-ids] *))) {
759
858
  outline: 2px solid var(--orionplace-accent);
760
859
  outline-offset: 3px;
761
860
  }
762
861
 
763
- .op-feature-prose :deep(:where(img, video)) {
862
+ .op-feature-prose :deep(:where(img, video):where(:not([data-product-id] *, [data-product-ids] *))) {
764
863
  display: block;
765
864
  max-width: 100%;
766
865
  height: auto;
@@ -770,31 +869,31 @@
770
869
  /* A narrow article has no room for text beside a floated image; 592px is
771
870
  the article's width in the /journal page on a 640px screen. */
772
871
  @container article (max-width: 591px) {
773
- .op-feature-prose :deep(:where(img, video)[style*='float']) {
872
+ .op-feature-prose :deep(:where(img, video)[style*='float']:where(:not([data-product-id] *, [data-product-ids] *))) {
774
873
  float: none !important;
775
874
  margin: 1rem auto !important;
776
875
  }
777
876
  }
778
877
 
779
- .op-feature-prose :deep(audio) {
878
+ .op-feature-prose :deep(audio:where(:not([data-product-id] *, [data-product-ids] *))) {
780
879
  display: block;
781
880
  width: 100%;
782
881
  }
783
882
 
784
- .op-feature-prose :deep(figcaption) {
883
+ .op-feature-prose :deep(figcaption:where(:not([data-product-id] *, [data-product-ids] *))) {
785
884
  margin-top: 0.5rem;
786
885
  font-size: 0.875rem;
787
886
  color: var(--orionplace-muted);
788
887
  }
789
888
 
790
- .op-feature-prose :deep(.blog-image-gallery__track) {
889
+ .op-feature-prose :deep(.blog-image-gallery__track:where(:not([data-product-id] *, [data-product-ids] *))) {
791
890
  display: flex;
792
891
  gap: 0.5rem;
793
892
  overflow-x: auto;
794
893
  scroll-snap-type: x mandatory;
795
894
  }
796
895
 
797
- .op-feature-prose :deep(.blog-image-gallery__track img) {
896
+ .op-feature-prose :deep(.blog-image-gallery__track img:where(:not([data-product-id] *, [data-product-ids] *))) {
798
897
  flex: 0 0 auto;
799
898
  width: auto;
800
899
  max-width: min(80%, 32rem);
@@ -803,104 +902,122 @@
803
902
  scroll-snap-align: start;
804
903
  }
805
904
 
806
- .op-feature-prose :deep(details) {
905
+ .op-feature-prose :deep(details:where(:not([data-product-id] *, [data-product-ids] *))) {
807
906
  margin: 1rem 0;
808
907
  padding: 0.75rem 1rem;
809
908
  border: 1px solid var(--orionplace-border);
810
909
  }
811
910
 
812
- .op-feature-prose :deep(summary) {
911
+ .op-feature-prose :deep(summary:where(:not([data-product-id] *, [data-product-ids] *))) {
813
912
  cursor: pointer;
814
913
  }
815
914
 
816
- .op-feature-prose :deep([data-type='math-inline']),
817
- .op-feature-prose :deep([data-type='math-block']) {
915
+ .op-feature-prose :deep([data-type='math-inline']:where(:not([data-product-id] *, [data-product-ids] *))),
916
+ .op-feature-prose :deep([data-type='math-block']:where(:not([data-product-id] *, [data-product-ids] *))) {
818
917
  font-family: ui-monospace, monospace;
819
918
  font-size: 0.9em;
820
919
  }
821
920
 
822
- .op-feature-prose :deep([data-type='math-block']) {
921
+ .op-feature-prose :deep([data-type='math-block']:where(:not([data-product-id] *, [data-product-ids] *))) {
823
922
  display: block;
824
923
  margin: 1rem 0;
825
924
  overflow-x: auto;
826
925
  text-align: center;
827
926
  }
828
927
 
829
- /* Product placements from the editor have no content in the text. */
928
+ /* Product placements: cards fill them; one without its products stays hidden. */
830
929
  .op-feature-prose :deep(:where([data-product-id], [data-product-ids]):empty) {
831
930
  display: none;
832
931
  }
833
932
 
933
+ .op-feature-prose :deep([data-product-ids]) {
934
+ clear: both;
935
+ margin: 1.5rem 0;
936
+ }
937
+
938
+ .op-feature-prose :deep([data-product-id]:not([data-product-ids])) {
939
+ width: min(100%, 260px);
940
+ margin-block: 1.5rem;
941
+ }
942
+
943
+ /* The editor's float has no room beside a card on a narrow page. */
944
+ @container article (max-width: 480px) {
945
+ .op-feature-prose :deep([data-product-id]:not([data-product-ids])) {
946
+ float: none !important;
947
+ margin-inline: auto !important;
948
+ }
949
+ }
950
+
834
951
  /* The editor's named colors, mixed with the theme text for contrast. */
835
- .op-feature-prose :deep([data-text-color='gray']) {
952
+ .op-feature-prose :deep([data-text-color='gray']:where(:not([data-product-id] *, [data-product-ids] *))) {
836
953
  color: color-mix(in srgb, gray 70%, var(--orionplace-text));
837
954
  }
838
955
 
839
- .op-feature-prose :deep([data-bg-color='gray']) {
956
+ .op-feature-prose :deep([data-bg-color='gray']:where(:not([data-product-id] *, [data-product-ids] *))) {
840
957
  background: color-mix(in srgb, gray 18%, transparent);
841
958
  }
842
959
 
843
- .op-feature-prose :deep([data-text-color='brown']) {
960
+ .op-feature-prose :deep([data-text-color='brown']:where(:not([data-product-id] *, [data-product-ids] *))) {
844
961
  color: color-mix(in srgb, brown 70%, var(--orionplace-text));
845
962
  }
846
963
 
847
- .op-feature-prose :deep([data-bg-color='brown']) {
964
+ .op-feature-prose :deep([data-bg-color='brown']:where(:not([data-product-id] *, [data-product-ids] *))) {
848
965
  background: color-mix(in srgb, brown 18%, transparent);
849
966
  }
850
967
 
851
- .op-feature-prose :deep([data-text-color='orange']) {
968
+ .op-feature-prose :deep([data-text-color='orange']:where(:not([data-product-id] *, [data-product-ids] *))) {
852
969
  color: color-mix(in srgb, orange 70%, var(--orionplace-text));
853
970
  }
854
971
 
855
- .op-feature-prose :deep([data-bg-color='orange']) {
972
+ .op-feature-prose :deep([data-bg-color='orange']:where(:not([data-product-id] *, [data-product-ids] *))) {
856
973
  background: color-mix(in srgb, orange 18%, transparent);
857
974
  }
858
975
 
859
- .op-feature-prose :deep([data-text-color='yellow']) {
976
+ .op-feature-prose :deep([data-text-color='yellow']:where(:not([data-product-id] *, [data-product-ids] *))) {
860
977
  color: color-mix(in srgb, yellow 70%, var(--orionplace-text));
861
978
  }
862
979
 
863
- .op-feature-prose :deep([data-bg-color='yellow']) {
980
+ .op-feature-prose :deep([data-bg-color='yellow']:where(:not([data-product-id] *, [data-product-ids] *))) {
864
981
  background: color-mix(in srgb, yellow 18%, transparent);
865
982
  }
866
983
 
867
- .op-feature-prose :deep([data-text-color='green']) {
984
+ .op-feature-prose :deep([data-text-color='green']:where(:not([data-product-id] *, [data-product-ids] *))) {
868
985
  color: color-mix(in srgb, green 70%, var(--orionplace-text));
869
986
  }
870
987
 
871
- .op-feature-prose :deep([data-bg-color='green']) {
988
+ .op-feature-prose :deep([data-bg-color='green']:where(:not([data-product-id] *, [data-product-ids] *))) {
872
989
  background: color-mix(in srgb, green 18%, transparent);
873
990
  }
874
991
 
875
- .op-feature-prose :deep([data-text-color='blue']) {
992
+ .op-feature-prose :deep([data-text-color='blue']:where(:not([data-product-id] *, [data-product-ids] *))) {
876
993
  color: color-mix(in srgb, blue 70%, var(--orionplace-text));
877
994
  }
878
995
 
879
- .op-feature-prose :deep([data-bg-color='blue']) {
996
+ .op-feature-prose :deep([data-bg-color='blue']:where(:not([data-product-id] *, [data-product-ids] *))) {
880
997
  background: color-mix(in srgb, blue 18%, transparent);
881
998
  }
882
999
 
883
- .op-feature-prose :deep([data-text-color='purple']) {
1000
+ .op-feature-prose :deep([data-text-color='purple']:where(:not([data-product-id] *, [data-product-ids] *))) {
884
1001
  color: color-mix(in srgb, purple 70%, var(--orionplace-text));
885
1002
  }
886
1003
 
887
- .op-feature-prose :deep([data-bg-color='purple']) {
1004
+ .op-feature-prose :deep([data-bg-color='purple']:where(:not([data-product-id] *, [data-product-ids] *))) {
888
1005
  background: color-mix(in srgb, purple 18%, transparent);
889
1006
  }
890
1007
 
891
- .op-feature-prose :deep([data-text-color='pink']) {
1008
+ .op-feature-prose :deep([data-text-color='pink']:where(:not([data-product-id] *, [data-product-ids] *))) {
892
1009
  color: color-mix(in srgb, pink 70%, var(--orionplace-text));
893
1010
  }
894
1011
 
895
- .op-feature-prose :deep([data-bg-color='pink']) {
1012
+ .op-feature-prose :deep([data-bg-color='pink']:where(:not([data-product-id] *, [data-product-ids] *))) {
896
1013
  background: color-mix(in srgb, pink 18%, transparent);
897
1014
  }
898
1015
 
899
- .op-feature-prose :deep([data-text-color='red']) {
1016
+ .op-feature-prose :deep([data-text-color='red']:where(:not([data-product-id] *, [data-product-ids] *))) {
900
1017
  color: color-mix(in srgb, red 70%, var(--orionplace-text));
901
1018
  }
902
1019
 
903
- .op-feature-prose :deep([data-bg-color='red']) {
1020
+ .op-feature-prose :deep([data-bg-color='red']:where(:not([data-product-id] *, [data-product-ids] *))) {
904
1021
  background: color-mix(in srgb, red 18%, transparent);
905
1022
  }
906
1023
  </style>
@@ -157,7 +157,7 @@
157
157
  :disabled="isPending"
158
158
  @click="openBranch(item.id)"
159
159
  >
160
- {{ labels.categories }} →
160
+ {{ labels.categories }} <span aria-hidden="true">→</span>
161
161
  </button>
162
162
  <NuxtLink
163
163
  class="op-feature-link op-feature-button op-feature-primary px-3!"
@@ -166,7 +166,7 @@
166
166
  query: { locale: selectionStore.localeCode },
167
167
  }"
168
168
  >
169
- {{ labels.products }} →
169
+ {{ labels.products }} <span aria-hidden="true">→</span>
170
170
  </NuxtLink>
171
171
  </li>
172
172
  </ul>
@@ -118,6 +118,9 @@
118
118
  :product-id="displayedProductId"
119
119
  :collection="collection"
120
120
  low-stock-label=""
121
+ :new-label="hiddenBadgeLabel('new')"
122
+ :popular-label="hiddenBadgeLabel('popular')"
123
+ :deal-label="hiddenBadgeLabel('deal')"
121
124
  class="pointer-events-none absolute"
122
125
  />
123
126
  <ProductCardActions
@@ -208,6 +211,9 @@
208
211
  :product-id="displayedProductId"
209
212
  :collection="collection"
210
213
  low-stock-label=""
214
+ :new-label="hiddenBadgeLabel('new')"
215
+ :popular-label="hiddenBadgeLabel('popular')"
216
+ :deal-label="hiddenBadgeLabel('deal')"
211
217
  />
212
218
  <ProductCardPrice
213
219
  data-product-part="ProductCardPrice"
@@ -359,6 +365,7 @@
359
365
  const card = ref<HTMLElement>();
360
366
  const { minHeight } = useOrionplaceStableHeight(card);
361
367
 
368
+ // hiddenBadges: badge keys not to show on this card (new / popular / deal).
362
369
  const props = withDefaults(
363
370
  defineProps<{
364
371
  productId: number;
@@ -369,6 +376,7 @@
369
376
  actionsDisabled?: boolean;
370
377
  purchaseActions?: boolean;
371
378
  variantProductIds?: number[];
379
+ hiddenBadges?: string[];
372
380
  }>(),
373
381
  {
374
382
  collection: 'products',
@@ -377,9 +385,15 @@
377
385
  disabled: false,
378
386
  actionsDisabled: false,
379
387
  purchaseActions: true,
388
+ hiddenBadges: () => [],
380
389
  },
381
390
  );
382
391
 
392
+ // An empty label hides the badge; undefined keeps the locale's label.
393
+ function hiddenBadgeLabel(key: string) {
394
+ return props.hiddenBadges.includes(key) ? '' : undefined;
395
+ }
396
+
383
397
  const store = useOrionplaceStore();
384
398
  const productStore = useOrionplaceProductsStore();
385
399
  const selectionStore = useOrionplaceSelectionsStore();
@@ -20,6 +20,7 @@
20
20
  :product-id="product.id"
21
21
  :collection="collection"
22
22
  :disabled="disabled"
23
+ :hidden-badges="hiddenBadges"
23
24
  @select="emit('select', $event)"
24
25
  @select-seller="emit('selectSeller', $event)"
25
26
  @select-variant="emit('selectVariant', $event)"
@@ -120,8 +121,9 @@
120
121
  limit?: number;
121
122
  productIds?: number[];
122
123
  layout?: 'responsive' | 'grid';
124
+ hiddenBadges?: string[];
123
125
  }>(),
124
- { collection: 'products', disabled: false, limit: 0, layout: 'responsive' },
126
+ { collection: 'products', disabled: false, limit: 0, layout: 'responsive', hiddenBadges: () => [] },
125
127
  );
126
128
 
127
129
  const store = useOrionplaceStore();