@citizenplane/pimp 18.17.0 → 18.19.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 (69) hide show
  1. package/dist/components/BaseInputLabel.vue.d.ts +1 -1
  2. package/dist/components/BaseInputLabel.vue.d.ts.map +1 -1
  3. package/dist/components/CpAncillaryCategory.vue.d.ts +42 -0
  4. package/dist/components/CpAncillaryCategory.vue.d.ts.map +1 -0
  5. package/dist/components/CpAncillaryItem.vue.d.ts +30 -0
  6. package/dist/components/CpAncillaryItem.vue.d.ts.map +1 -0
  7. package/dist/components/CpBottomSheet.vue.d.ts +1 -1
  8. package/dist/components/CpCheckbox.vue.d.ts +2 -2
  9. package/dist/components/CpCheckbox.vue.d.ts.map +1 -1
  10. package/dist/components/CpDialog.vue.d.ts +1 -1
  11. package/dist/components/CpDialog.vue.d.ts.map +1 -1
  12. package/dist/components/CpMenu.vue.d.ts +1 -1
  13. package/dist/components/CpMenu.vue.d.ts.map +1 -1
  14. package/dist/components/CpSelect.vue.d.ts +1 -1
  15. package/dist/components/CpSelect.vue.d.ts.map +1 -1
  16. package/dist/components/CpSwitch.vue.d.ts +4 -4
  17. package/dist/components/CpSwitch.vue.d.ts.map +1 -1
  18. package/dist/components/CpTelInput.vue.d.ts +2 -2
  19. package/dist/components/CpTelInput.vue.d.ts.map +1 -1
  20. package/dist/components/CpTooltip.vue.d.ts +1 -1
  21. package/dist/components/CpTooltip.vue.d.ts.map +1 -1
  22. package/dist/components/index.d.ts +5 -1
  23. package/dist/components/index.d.ts.map +1 -1
  24. package/dist/constants/CpAncillaryCategoryTypes.d.ts +12 -0
  25. package/dist/constants/CpAncillaryCategoryTypes.d.ts.map +1 -0
  26. package/dist/constants/CpAncillaryItemTypes.d.ts +31 -0
  27. package/dist/constants/CpAncillaryItemTypes.d.ts.map +1 -0
  28. package/dist/pimp.es.js +1980 -1781
  29. package/dist/pimp.umd.js +48 -48
  30. package/dist/style.css +1 -1
  31. package/package.json +1 -1
  32. package/src/assets/images/ancillaries/airport-transfer.png +0 -0
  33. package/src/assets/images/ancillaries/bicycle.png +0 -0
  34. package/src/assets/images/ancillaries/cabin-bag.png +0 -0
  35. package/src/assets/images/ancillaries/checked-bag-size-1.png +0 -0
  36. package/src/assets/images/ancillaries/checked-bag-size-2.png +0 -0
  37. package/src/assets/images/ancillaries/checked-bag-size-3.png +0 -0
  38. package/src/assets/images/ancillaries/checked-bag.png +0 -0
  39. package/src/assets/images/ancillaries/connectivity.png +0 -0
  40. package/src/assets/images/ancillaries/diving-set.png +0 -0
  41. package/src/assets/images/ancillaries/extra-weight.png +0 -0
  42. package/src/assets/images/ancillaries/fast-track.png +0 -0
  43. package/src/assets/images/ancillaries/firearm.png +0 -0
  44. package/src/assets/images/ancillaries/fishing-rod.png +0 -0
  45. package/src/assets/images/ancillaries/golf.png +0 -0
  46. package/src/assets/images/ancillaries/hand-bag.png +0 -0
  47. package/src/assets/images/ancillaries/kite-surf.png +0 -0
  48. package/src/assets/images/ancillaries/lounge-access.png +0 -0
  49. package/src/assets/images/ancillaries/meet-and-greet.png +0 -0
  50. package/src/assets/images/ancillaries/music.png +0 -0
  51. package/src/assets/images/ancillaries/oversized.png +0 -0
  52. package/src/assets/images/ancillaries/pet-in-cabin.png +0 -0
  53. package/src/assets/images/ancillaries/pet-in-hold.png +0 -0
  54. package/src/assets/images/ancillaries/skis.png +0 -0
  55. package/src/assets/images/ancillaries/special-meal.png +0 -0
  56. package/src/assets/images/ancillaries/standard-meal.png +0 -0
  57. package/src/assets/images/ancillary-categories/baggages.png +0 -0
  58. package/src/assets/images/ancillary-categories/carbon-offset.png +0 -0
  59. package/src/assets/images/ancillary-categories/insurance.png +0 -0
  60. package/src/assets/images/ancillary-categories/pets.png +0 -0
  61. package/src/assets/images/ancillary-categories/restricted.png +0 -0
  62. package/src/assets/images/ancillary-categories/sport-equipments.png +0 -0
  63. package/src/components/CpAncillaryCategory.vue +311 -0
  64. package/src/components/CpAncillaryItem.vue +275 -0
  65. package/src/components/index.ts +8 -0
  66. package/src/constants/CpAncillaryCategoryTypes.ts +14 -0
  67. package/src/constants/CpAncillaryItemTypes.ts +30 -0
  68. package/src/stories/CpAncillaryCategory.stories.ts +274 -0
  69. package/src/stories/CpAncillaryItem.stories.ts +243 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@citizenplane/pimp",
3
- "version": "18.17.0",
3
+ "version": "18.19.0",
4
4
  "scripts": {
5
5
  "dev": "storybook dev -p 8081",
6
6
  "build-storybook": "storybook build --output-dir ./docs",
@@ -0,0 +1,311 @@
1
+ <template>
2
+ <div class="cpAncillaryCategory">
3
+ <div class="cpAncillaryCategory__header">
4
+ <button class="cpAncillaryCategory__lead" type="button" @click="handleToggle">
5
+ <div class="cpAncillaryCategory__visual">
6
+ <img
7
+ v-if="visual"
8
+ alt=""
9
+ class="cpAncillaryCategory__visualImage"
10
+ :class="visualImageDynamicClass"
11
+ :src="visual"
12
+ />
13
+ <cp-icon v-else class="cpAncillaryCategory__fallbackIcon" size="40" type="shopping-bag" />
14
+ </div>
15
+ <div class="cpAncillaryCategory__texts">
16
+ <span :id="titleId" class="cpAncillaryCategory__title">{{ title }}</span>
17
+ <span class="cpAncillaryCategory__description">{{ description }}</span>
18
+ </div>
19
+ </button>
20
+ <div v-if="isSwitch" class="cpAncillaryCategory__trailing">
21
+ <span v-if="hasFromPrice" class="cpAncillaryCategory__price">{{ formattedPrice }}</span>
22
+ <cp-switch v-model="checked" color="accent" />
23
+ </div>
24
+ <div v-else-if="isActuallyExpandable" class="cpAncillaryCategory__trailing">
25
+ <span v-if="hasFromPrice" class="cpAncillaryCategory__fromPriceClip">
26
+ <span :aria-hidden="isOpen" class="cpAncillaryCategory__fromPrice" :class="fromPriceDynamicClass">
27
+ <span class="cpAncillaryCategory__fromPriceLabel">{{ fromPriceLabel }}</span>
28
+ <span class="cpAncillaryCategory__fromPriceAmount">{{ formattedPrice }}</span>
29
+ </span>
30
+ </span>
31
+ <button
32
+ :aria-controls="bodyId"
33
+ :aria-expanded="isOpen"
34
+ :aria-labelledby="titleId"
35
+ class="cpAncillaryCategory__toggle"
36
+ type="button"
37
+ @click="handleToggle"
38
+ >
39
+ <cp-icon class="cpAncillaryCategory__chevron" :class="chevronDynamicClass" size="24" type="chevron-down" />
40
+ </button>
41
+ </div>
42
+ </div>
43
+ <cp-transition-expand v-if="isActuallyExpandable">
44
+ <div v-if="isOpen">
45
+ <div :id="bodyId" class="cpAncillaryCategory__body">
46
+ <slot />
47
+ </div>
48
+ </div>
49
+ </cp-transition-expand>
50
+ <div v-else-if="!isSwitch" class="cpAncillaryCategory__body">
51
+ <slot />
52
+ </div>
53
+ </div>
54
+ </template>
55
+
56
+ <script setup lang="ts">
57
+ import { computed, ref, useId } from 'vue'
58
+
59
+ import CpIcon from '@/components/CpIcon.vue'
60
+ import CpSwitch from '@/components/CpSwitch.vue'
61
+ import CpTransitionExpand from '@/components/CpTransitionExpand.vue'
62
+
63
+ import BaggagesVisual from '@/assets/images/ancillary-categories/baggages.png'
64
+ import CarbonOffsetVisual from '@/assets/images/ancillary-categories/carbon-offset.png'
65
+ import InsuranceVisual from '@/assets/images/ancillary-categories/insurance.png'
66
+ import PetsVisual from '@/assets/images/ancillary-categories/pets.png'
67
+ import RestrictedVisual from '@/assets/images/ancillary-categories/restricted.png'
68
+ import SportEquipmentsVisual from '@/assets/images/ancillary-categories/sport-equipments.png'
69
+ import { CpAncillaryCategoryTypes, isSwitchCategory } from '@/constants/CpAncillaryCategoryTypes'
70
+
71
+ interface Props {
72
+ currency?: string
73
+ description: string
74
+ fromPrice?: number
75
+ fromPriceLabel?: string
76
+ isDefaultClosed?: boolean
77
+ isExpandable?: boolean
78
+ locale?: string
79
+ title: string
80
+ type: CpAncillaryCategoryTypes
81
+ }
82
+
83
+ const props = withDefaults(defineProps<Props>(), {
84
+ fromPriceLabel: 'From',
85
+ isDefaultClosed: false,
86
+ isExpandable: false,
87
+ locale: () => (typeof navigator === 'undefined' ? 'en' : navigator.language),
88
+ })
89
+
90
+ const checked = defineModel<boolean>('checked', { default: false })
91
+
92
+ const isOpen = ref(!props.isDefaultClosed)
93
+
94
+ const isSwitch = computed(() => isSwitchCategory(props.type))
95
+ const isActuallyExpandable = computed(() => props.isExpandable && !isSwitch.value)
96
+
97
+ const toggleId = useId()
98
+ const titleId = computed(() => `${toggleId}-title`)
99
+ const bodyId = computed(() => `${toggleId}-body`)
100
+
101
+ const categoryVisuals: Partial<Record<CpAncillaryCategoryTypes, string>> = {
102
+ [CpAncillaryCategoryTypes.BAGGAGES]: BaggagesVisual,
103
+ [CpAncillaryCategoryTypes.CARBON_OFFSET]: CarbonOffsetVisual,
104
+ [CpAncillaryCategoryTypes.INSURANCE]: InsuranceVisual,
105
+ [CpAncillaryCategoryTypes.PETS]: PetsVisual,
106
+ [CpAncillaryCategoryTypes.RESTRICTED]: RestrictedVisual,
107
+ [CpAncillaryCategoryTypes.SPORT_EQUIPMENTS]: SportEquipmentsVisual,
108
+ }
109
+
110
+ const visual = computed(() => categoryVisuals[props.type] ?? null)
111
+
112
+ const visualImageDynamicClass = computed(() => ({
113
+ 'cpAncillaryCategory__visualImage--isBaggages': props.type === CpAncillaryCategoryTypes.BAGGAGES,
114
+ }))
115
+
116
+ const formattedPrice = computed(() => {
117
+ if (props.fromPrice === undefined || !props.currency) return null
118
+ return new Intl.NumberFormat(props.locale, { style: 'currency', currency: props.currency }).format(
119
+ props.fromPrice / 100,
120
+ )
121
+ })
122
+
123
+ const hasFromPrice = computed(() => formattedPrice.value !== null)
124
+
125
+ const fromPriceDynamicClass = computed(() => ({ 'cpAncillaryCategory__fromPrice--isHidden': isOpen.value }))
126
+
127
+ const chevronDynamicClass = computed(() => ({ 'cpAncillaryCategory__chevron--isRotated': isOpen.value }))
128
+
129
+ const handleToggle = () => {
130
+ isOpen.value = !isOpen.value
131
+ }
132
+ </script>
133
+
134
+ <style lang="scss">
135
+ .cpAncillaryCategory {
136
+ display: flex;
137
+ box-sizing: border-box;
138
+ flex-direction: column;
139
+ padding: var(--cp-spacing-sm);
140
+ border: fn.px-to-rem(1) solid var(--cp-border-soft);
141
+ border-radius: var(--cp-radius-xl);
142
+ background: var(--cp-background-primary);
143
+ box-shadow: var(--cp-shadows-xs);
144
+ gap: var(--cp-spacing-sm);
145
+
146
+ &__header {
147
+ display: flex;
148
+ width: 100%;
149
+ align-items: center;
150
+ padding: var(--cp-spacing-lg);
151
+ border-radius: var(--cp-radius-lg);
152
+ gap: var(--cp-spacing-lg);
153
+ }
154
+
155
+ &__trailing {
156
+ display: flex;
157
+ flex: none;
158
+ align-items: center;
159
+ justify-content: flex-end;
160
+ padding: 0 var(--cp-spacing-xl);
161
+ border-radius: var(--cp-radius-lg);
162
+ gap: var(--cp-spacing-lg);
163
+ }
164
+
165
+ &__price {
166
+ color: var(--cp-text-accent-primary);
167
+ font-size: var(--cp-text-size-lg);
168
+ font-variant-numeric: lining-nums tabular-nums;
169
+ font-weight: 600;
170
+ line-height: var(--cp-text-lg-line-height);
171
+ }
172
+
173
+ &__fromPriceClip {
174
+ overflow: hidden;
175
+ }
176
+
177
+ &__fromPrice {
178
+ display: flex;
179
+ align-items: center;
180
+ gap: var(--cp-spacing-md);
181
+ opacity: 1;
182
+ transform: translate3d(0, 0, 0);
183
+ transition:
184
+ opacity 200ms ease,
185
+ transform 200ms ease;
186
+
187
+ &--isHidden {
188
+ opacity: 0;
189
+ pointer-events: none;
190
+ transform: translate3d(0, 20px, 0);
191
+ }
192
+ }
193
+
194
+ &__fromPriceLabel {
195
+ color: var(--cp-text-secondary);
196
+ font-size: var(--cp-text-size-lg);
197
+ font-variant-numeric: lining-nums tabular-nums;
198
+ line-height: var(--cp-text-lg-line-height);
199
+ }
200
+
201
+ &__fromPriceAmount {
202
+ color: var(--cp-text-primary);
203
+ font-size: var(--cp-text-size-lg);
204
+ font-variant-numeric: lining-nums tabular-nums;
205
+ font-weight: 600;
206
+ line-height: var(--cp-text-lg-line-height);
207
+ }
208
+
209
+ &__toggle {
210
+ display: flex;
211
+ flex: none;
212
+ align-items: center;
213
+ justify-content: center;
214
+ padding: var(--cp-spacing-xs);
215
+ border: 0;
216
+ border-radius: 50%;
217
+ appearance: none;
218
+ background: transparent;
219
+ color: inherit;
220
+ cursor: pointer;
221
+ transition: background-color 200ms ease;
222
+
223
+ @media (hover: hover) {
224
+ &:hover {
225
+ background: var(--cp-background-secondary-hover);
226
+ }
227
+ }
228
+
229
+ &:focus-visible {
230
+ border-radius: 50%;
231
+ background: var(--cp-background-secondary-hover);
232
+
233
+ @extend %u-focus-outline;
234
+ }
235
+ }
236
+
237
+ &__lead {
238
+ @extend %u-focus-outline;
239
+
240
+ display: flex;
241
+ width: 100%;
242
+ min-width: 0;
243
+ flex: auto;
244
+ align-items: center;
245
+ gap: var(--cp-spacing-xl);
246
+ text-align: left;
247
+ }
248
+
249
+ &__visual {
250
+ display: flex;
251
+ width: fn.px-to-rem(96);
252
+ height: fn.px-to-rem(96);
253
+ flex: none;
254
+ align-items: center;
255
+ justify-content: center;
256
+ }
257
+
258
+ &__visualImage {
259
+ width: fn.px-to-rem(72);
260
+ height: fn.px-to-rem(72);
261
+ object-fit: contain;
262
+
263
+ &--isBaggages {
264
+ width: fn.px-to-rem(84);
265
+ height: fn.px-to-rem(84);
266
+ }
267
+ }
268
+
269
+ &__fallbackIcon {
270
+ color: var(--cp-text-tertiary);
271
+ }
272
+
273
+ &__texts {
274
+ display: flex;
275
+ min-width: 0;
276
+ flex-direction: column;
277
+ }
278
+
279
+ &__title {
280
+ color: var(--cp-text-accent-primary);
281
+ font-size: var(--cp-text-size-xl);
282
+ font-weight: 600;
283
+ line-height: var(--cp-text-xl-line-height);
284
+ }
285
+
286
+ &__description {
287
+ color: var(--cp-text-primary);
288
+ font-size: var(--cp-text-size-md);
289
+ line-height: var(--cp-text-md-line-height);
290
+ }
291
+
292
+ &__chevron {
293
+ flex: none;
294
+ color: var(--cp-text-accent-primary);
295
+ transition: rotate 300ms ease;
296
+
297
+ &--isRotated {
298
+ rotate: 180deg;
299
+ }
300
+ }
301
+
302
+ &__body {
303
+ display: flex;
304
+ flex-direction: column;
305
+ padding: var(--cp-spacing-lg);
306
+ border-radius: var(--cp-radius-lg);
307
+ background: var(--cp-background-primary);
308
+ gap: var(--cp-spacing-lg);
309
+ }
310
+ }
311
+ </style>
@@ -0,0 +1,275 @@
1
+ <template>
2
+ <div class="cpAncillaryItem" :class="cpAncillaryItemDynamicClass">
3
+ <div class="cpAncillaryItem__wrap">
4
+ <div class="cpAncillaryItem__visual">
5
+ <img v-if="ancillaryImage" alt="" class="cpAncillaryItem__image" :src="ancillaryImage" />
6
+ <cp-icon v-else class="cpAncillaryItem__fallbackIcon" size="40" type="shopping-bag" />
7
+ </div>
8
+ <div class="cpAncillaryItem__content">
9
+ <div class="cpAncillaryItem__name">
10
+ <p class="cpAncillaryItem__title">{{ title }}</p>
11
+ <p class="cpAncillaryItem__description">{{ description }}</p>
12
+ <p v-if="subtitle" class="cpAncillaryItem__subtitle">{{ subtitle }}</p>
13
+ </div>
14
+ <div class="cpAncillaryItem__quantity">
15
+ <cp-quantity-counter
16
+ v-model="quantity"
17
+ class="cpAncillaryItem__counter"
18
+ :decrease-label="decreaseLabel"
19
+ :increase-label="increaseLabel"
20
+ :max="maxQuantity"
21
+ />
22
+ <span v-if="hasMaxQuantity" class="cpAncillaryItem__maxQuantity">{{ maxQuantityLabel }}</span>
23
+ </div>
24
+ <p v-if="formattedPrice" class="cpAncillaryItem__price">{{ formattedPrice }}</p>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ </template>
29
+
30
+ <script setup lang="ts">
31
+ import { computed } from 'vue'
32
+
33
+ import CpIcon from '@/components/CpIcon.vue'
34
+ import CpQuantityCounter from '@/components/CpQuantityCounter.vue'
35
+
36
+ import AirportTransferImage from '@/assets/images/ancillaries/airport-transfer.png'
37
+ import BicycleImage from '@/assets/images/ancillaries/bicycle.png'
38
+ import CabinBagImage from '@/assets/images/ancillaries/cabin-bag.png'
39
+ import CheckedBagImage from '@/assets/images/ancillaries/checked-bag.png'
40
+ import CheckedBagSize1Image from '@/assets/images/ancillaries/checked-bag-size-1.png'
41
+ import CheckedBagSize2Image from '@/assets/images/ancillaries/checked-bag-size-2.png'
42
+ import CheckedBagSize3Image from '@/assets/images/ancillaries/checked-bag-size-3.png'
43
+ import ConnectivityImage from '@/assets/images/ancillaries/connectivity.png'
44
+ import DivingSetImage from '@/assets/images/ancillaries/diving-set.png'
45
+ import ExtraWeightImage from '@/assets/images/ancillaries/extra-weight.png'
46
+ import FastTrackImage from '@/assets/images/ancillaries/fast-track.png'
47
+ import FirearmImage from '@/assets/images/ancillaries/firearm.png'
48
+ import FishingRodImage from '@/assets/images/ancillaries/fishing-rod.png'
49
+ import GolfImage from '@/assets/images/ancillaries/golf.png'
50
+ import HandBagImage from '@/assets/images/ancillaries/hand-bag.png'
51
+ import KiteSurfImage from '@/assets/images/ancillaries/kite-surf.png'
52
+ import LoungeAccessImage from '@/assets/images/ancillaries/lounge-access.png'
53
+ import MeetAndGreetImage from '@/assets/images/ancillaries/meet-and-greet.png'
54
+ import MusicImage from '@/assets/images/ancillaries/music.png'
55
+ import OversizedImage from '@/assets/images/ancillaries/oversized.png'
56
+ import PetInCabinImage from '@/assets/images/ancillaries/pet-in-cabin.png'
57
+ import PetInHoldImage from '@/assets/images/ancillaries/pet-in-hold.png'
58
+ import SkisImage from '@/assets/images/ancillaries/skis.png'
59
+ import SpecialMealImage from '@/assets/images/ancillaries/special-meal.png'
60
+ import StandardMealImage from '@/assets/images/ancillaries/standard-meal.png'
61
+ import { CpAncillaryItemTypes } from '@/constants/CpAncillaryItemTypes'
62
+
63
+ interface Props {
64
+ currency?: string
65
+ description: string
66
+ locale?: string
67
+ maxQuantity?: number | null
68
+ maxQuantityLabel?: string
69
+ price?: number
70
+ subtitle?: string
71
+ title: string
72
+ type: CpAncillaryItemTypes
73
+ vertical?: boolean
74
+ }
75
+
76
+ const props = withDefaults(defineProps<Props>(), {
77
+ locale: () => (typeof navigator === 'undefined' ? 'en' : navigator.language),
78
+ maxQuantity: null,
79
+ vertical: false,
80
+ })
81
+
82
+ const decreaseLabel = 'Decrease quantity'
83
+ const increaseLabel = 'Increase quantity'
84
+
85
+ const quantity = defineModel<number>('quantity', { required: true })
86
+
87
+ const ancillaryImages: Partial<Record<CpAncillaryItemTypes, string>> = {
88
+ [CpAncillaryItemTypes.AIRPORT_TRANSFER]: AirportTransferImage,
89
+ [CpAncillaryItemTypes.BICYCLE]: BicycleImage,
90
+ [CpAncillaryItemTypes.CABIN_BAG]: CabinBagImage,
91
+ [CpAncillaryItemTypes.CHECKED_BAG]: CheckedBagImage,
92
+ [CpAncillaryItemTypes.CHECKED_BAG_SIZE_1]: CheckedBagSize1Image,
93
+ [CpAncillaryItemTypes.CHECKED_BAG_SIZE_2]: CheckedBagSize2Image,
94
+ [CpAncillaryItemTypes.CHECKED_BAG_SIZE_3]: CheckedBagSize3Image,
95
+ [CpAncillaryItemTypes.CONNECTIVITY]: ConnectivityImage,
96
+ [CpAncillaryItemTypes.DIVING_SET]: DivingSetImage,
97
+ [CpAncillaryItemTypes.EXTRA_WEIGHT]: ExtraWeightImage,
98
+ [CpAncillaryItemTypes.FAST_TRACK]: FastTrackImage,
99
+ [CpAncillaryItemTypes.FIREARM]: FirearmImage,
100
+ [CpAncillaryItemTypes.FISHING_ROD]: FishingRodImage,
101
+ [CpAncillaryItemTypes.GOLF]: GolfImage,
102
+ [CpAncillaryItemTypes.HAND_BAG]: HandBagImage,
103
+ [CpAncillaryItemTypes.KITE_SURF]: KiteSurfImage,
104
+ [CpAncillaryItemTypes.LOUNGE_ACCESS]: LoungeAccessImage,
105
+ [CpAncillaryItemTypes.MEET_AND_GREET]: MeetAndGreetImage,
106
+ [CpAncillaryItemTypes.MUSIC]: MusicImage,
107
+ [CpAncillaryItemTypes.OVERSIZED]: OversizedImage,
108
+ [CpAncillaryItemTypes.PET_IN_CABIN]: PetInCabinImage,
109
+ [CpAncillaryItemTypes.PET_IN_HOLD]: PetInHoldImage,
110
+ [CpAncillaryItemTypes.SKIS]: SkisImage,
111
+ [CpAncillaryItemTypes.SPECIAL_MEAL]: SpecialMealImage,
112
+ [CpAncillaryItemTypes.STANDARD_MEAL]: StandardMealImage,
113
+ }
114
+
115
+ const ancillaryImage = computed(() => ancillaryImages[props.type] ?? null)
116
+
117
+ const hasMaxQuantity = computed(() => props.maxQuantity !== null)
118
+
119
+ const formattedPrice = computed(() => {
120
+ if (props.price === undefined || !props.currency) return null
121
+ return new Intl.NumberFormat(props.locale, { style: 'currency', currency: props.currency }).format(props.price / 100)
122
+ })
123
+
124
+ const isSelected = computed(() => quantity.value > 0)
125
+ const cpAncillaryItemDynamicClass = computed(() => ({
126
+ 'cpAncillaryItem--isVertical': props.vertical,
127
+ 'cpAncillaryItem--isSelected': isSelected.value,
128
+ }))
129
+ </script>
130
+
131
+ <style lang="scss">
132
+ .cpAncillaryItem {
133
+ width: 100%;
134
+ box-sizing: border-box;
135
+ padding: var(--cp-spacing-sm);
136
+ border: fn.px-to-rem(1) solid var(--cp-border-soft);
137
+ border-radius: var(--cp-radius-md);
138
+ background: var(--cp-background-primary);
139
+
140
+ &__wrap {
141
+ display: flex;
142
+ height: 100%;
143
+ border-radius: var(--cp-radius-sm);
144
+ gap: var(--cp-spacing-lg);
145
+ }
146
+
147
+ &__visual {
148
+ display: flex;
149
+ overflow: hidden;
150
+ width: fn.px-to-rem(96);
151
+ flex: none;
152
+ align-items: center;
153
+ justify-content: center;
154
+ border-radius: var(--cp-radius-sm);
155
+ background: var(--cp-background-secondary);
156
+ }
157
+
158
+ &__image {
159
+ width: fn.px-to-rem(96);
160
+ height: fn.px-to-rem(96);
161
+ object-fit: contain;
162
+ transition: transform 200ms ease;
163
+ }
164
+
165
+ &__fallbackIcon {
166
+ color: var(--cp-text-tertiary);
167
+ }
168
+
169
+ &__content {
170
+ display: flex;
171
+ min-width: 0;
172
+ flex: auto;
173
+ flex-direction: column;
174
+ padding: var(--cp-spacing-lg) var(--cp-spacing-lg) var(--cp-spacing-lg) 0;
175
+ gap: var(--cp-spacing-lg);
176
+ }
177
+
178
+ &__name {
179
+ display: flex;
180
+ min-width: 0;
181
+ flex-direction: column;
182
+ }
183
+
184
+ &__title {
185
+ color: var(--cp-text-primary);
186
+ font-size: var(--cp-text-size-md);
187
+ font-weight: 600;
188
+ line-height: var(--cp-text-md-line-height);
189
+ }
190
+
191
+ &__description,
192
+ &__subtitle {
193
+ color: var(--cp-text-secondary);
194
+ font-size: var(--cp-text-size-sm);
195
+ line-height: var(--cp-text-sm-line-height);
196
+ }
197
+
198
+ &__quantity {
199
+ display: flex;
200
+ align-items: center;
201
+ gap: var(--cp-spacing-md);
202
+ }
203
+
204
+ &__maxQuantity {
205
+ color: var(--cp-text-tertiary);
206
+ font-size: var(--cp-text-size-xs);
207
+ line-height: var(--cp-text-xs-line-height);
208
+ }
209
+
210
+ &__price {
211
+ color: var(--cp-text-accent-primary);
212
+ font-size: var(--cp-text-size-md);
213
+ font-weight: 600;
214
+ line-height: var(--cp-text-md-line-height);
215
+ }
216
+
217
+ &--isVertical {
218
+ .cpAncillaryItem__wrap {
219
+ flex-direction: column;
220
+ align-items: center;
221
+ padding-bottom: var(--cp-spacing-lg);
222
+ }
223
+
224
+ .cpAncillaryItem__visual {
225
+ width: 100%;
226
+ height: fn.px-to-rem(128);
227
+ }
228
+
229
+ .cpAncillaryItem__image {
230
+ width: fn.px-to-rem(80);
231
+ height: fn.px-to-rem(80);
232
+ }
233
+
234
+ .cpAncillaryItem__content {
235
+ width: 100%;
236
+ align-items: center;
237
+ }
238
+
239
+ .cpAncillaryItem__name {
240
+ padding: 0 var(--cp-spacing-lg);
241
+ }
242
+
243
+ .cpAncillaryItem__title,
244
+ .cpAncillaryItem__description,
245
+ .cpAncillaryItem__subtitle {
246
+ text-align: center;
247
+ }
248
+
249
+ .cpAncillaryItem__quantity {
250
+ flex-direction: column;
251
+ gap: var(--cp-spacing-sm);
252
+ }
253
+ }
254
+
255
+ &--isSelected {
256
+ border-color: var(--cp-border-accent-solid);
257
+ outline: fn.px-to-rem(1) solid var(--cp-border-accent-solid);
258
+ outline-offset: fn.px-to-rem(-2);
259
+
260
+ .cpAncillaryItem__wrap {
261
+ background: var(--cp-background-secondary);
262
+ }
263
+
264
+ .cpAncillaryItem__counter {
265
+ background: var(--cp-background-primary);
266
+ }
267
+ }
268
+
269
+ @media (hover: hover) {
270
+ &:hover .cpAncillaryItem__image {
271
+ transform: scale3d(1.1, 1.1, 1);
272
+ }
273
+ }
274
+ }
275
+ </style>