@citizenplane/pimp 18.17.0 → 18.18.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 (50) hide show
  1. package/dist/components/CpAncillaryItem.vue.d.ts +30 -0
  2. package/dist/components/CpAncillaryItem.vue.d.ts.map +1 -0
  3. package/dist/components/CpBottomSheet.vue.d.ts +1 -1
  4. package/dist/components/CpDate.vue.d.ts +1 -1
  5. package/dist/components/CpDate.vue.d.ts.map +1 -1
  6. package/dist/components/CpDialog.vue.d.ts +1 -1
  7. package/dist/components/CpDialog.vue.d.ts.map +1 -1
  8. package/dist/components/CpSwitch.vue.d.ts +1 -1
  9. package/dist/components/CpSwitch.vue.d.ts.map +1 -1
  10. package/dist/components/CpTextarea.vue.d.ts +1 -1
  11. package/dist/components/CpTextarea.vue.d.ts.map +1 -1
  12. package/dist/components/CpTransitionCounter.vue.d.ts +1 -1
  13. package/dist/components/CpTransitionCounter.vue.d.ts.map +1 -1
  14. package/dist/components/index.d.ts +3 -1
  15. package/dist/components/index.d.ts.map +1 -1
  16. package/dist/constants/CpAncillaryItemTypes.d.ts +31 -0
  17. package/dist/constants/CpAncillaryItemTypes.d.ts.map +1 -0
  18. package/dist/pimp.es.js +1720 -1626
  19. package/dist/pimp.umd.js +45 -45
  20. package/dist/style.css +1 -1
  21. package/package.json +1 -1
  22. package/src/assets/images/ancillaries/airport-transfer.png +0 -0
  23. package/src/assets/images/ancillaries/bicycle.png +0 -0
  24. package/src/assets/images/ancillaries/cabin-bag.png +0 -0
  25. package/src/assets/images/ancillaries/checked-bag-size-1.png +0 -0
  26. package/src/assets/images/ancillaries/checked-bag-size-2.png +0 -0
  27. package/src/assets/images/ancillaries/checked-bag-size-3.png +0 -0
  28. package/src/assets/images/ancillaries/checked-bag.png +0 -0
  29. package/src/assets/images/ancillaries/connectivity.png +0 -0
  30. package/src/assets/images/ancillaries/diving-set.png +0 -0
  31. package/src/assets/images/ancillaries/extra-weight.png +0 -0
  32. package/src/assets/images/ancillaries/fast-track.png +0 -0
  33. package/src/assets/images/ancillaries/firearm.png +0 -0
  34. package/src/assets/images/ancillaries/fishing-rod.png +0 -0
  35. package/src/assets/images/ancillaries/golf.png +0 -0
  36. package/src/assets/images/ancillaries/hand-bag.png +0 -0
  37. package/src/assets/images/ancillaries/kite-surf.png +0 -0
  38. package/src/assets/images/ancillaries/lounge-access.png +0 -0
  39. package/src/assets/images/ancillaries/meet-and-greet.png +0 -0
  40. package/src/assets/images/ancillaries/music.png +0 -0
  41. package/src/assets/images/ancillaries/oversized.png +0 -0
  42. package/src/assets/images/ancillaries/pet-in-cabin.png +0 -0
  43. package/src/assets/images/ancillaries/pet-in-hold.png +0 -0
  44. package/src/assets/images/ancillaries/skis.png +0 -0
  45. package/src/assets/images/ancillaries/special-meal.png +0 -0
  46. package/src/assets/images/ancillaries/standard-meal.png +0 -0
  47. package/src/components/CpAncillaryItem.vue +275 -0
  48. package/src/components/index.ts +4 -0
  49. package/src/constants/CpAncillaryItemTypes.ts +30 -0
  50. 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.18.0",
4
4
  "scripts": {
5
5
  "dev": "storybook dev -p 8081",
6
6
  "build-storybook": "storybook build --output-dir ./docs",
@@ -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>
@@ -19,6 +19,7 @@ import CpAccordion from './CpAccordion.vue'
19
19
  import CpAccordionGroup from './CpAccordionGroup.vue'
20
20
  import CpAirlineLogo from './CpAirlineLogo.vue'
21
21
  import CpAlert from './CpAlert.vue'
22
+ import CpAncillaryItem from './CpAncillaryItem.vue'
22
23
  import CpBadge from './CpBadge.vue'
23
24
  import CpBottomSheet from './CpBottomSheet.vue'
24
25
  import CpButton from './CpButton.vue'
@@ -114,6 +115,7 @@ const Components = {
114
115
  CpRadioNew,
115
116
  CpCalendar,
116
117
  CpAlert,
118
+ CpAncillaryItem,
117
119
  CpLoader,
118
120
  CpInput,
119
121
  CpText,
@@ -207,6 +209,7 @@ export type {
207
209
  FlightTerminal,
208
210
  } from '@/constants/tripDetails'
209
211
 
212
+ export { CpAncillaryItemTypes } from '@/constants/CpAncillaryItemTypes'
210
213
  export {
211
214
  CpSeatMapTransitionDirections,
212
215
  DEFAULT_CP_SEAT_MAP_CONFIG,
@@ -224,6 +227,7 @@ export {
224
227
  CpAccordionGroup,
225
228
  CpAirlineLogo,
226
229
  CpAlert,
230
+ CpAncillaryItem,
227
231
  CpBadge,
228
232
  CpBottomSheet,
229
233
  CpButton,
@@ -0,0 +1,30 @@
1
+ export enum CpAncillaryItemTypes {
2
+ AIRPORT_TRANSFER = 'airportTransfer',
3
+ BICYCLE = 'bicycle',
4
+ CABIN_BAG = 'cabinBag',
5
+ CANCELLATION_INSURANCE = 'cancellationInsurance',
6
+ CHECKED_BAG = 'checkedBag',
7
+ CHECKED_BAG_SIZE_1 = 'checkedBagSize1',
8
+ CHECKED_BAG_SIZE_2 = 'checkedBagSize2',
9
+ CHECKED_BAG_SIZE_3 = 'checkedBagSize3',
10
+ CONNECTIVITY = 'connectivity',
11
+ DIVING_SET = 'divingSet',
12
+ EXTRA_WEIGHT = 'extraWeight',
13
+ FAST_TRACK = 'fastTrack',
14
+ FIREARM = 'firearm',
15
+ FISHING_ROD = 'fishingRod',
16
+ GOLF = 'golf',
17
+ HAND_BAG = 'handBag',
18
+ KITE_SURF = 'kiteSurf',
19
+ LOUNGE_ACCESS = 'loungeAccess',
20
+ MEET_AND_GREET = 'meetAndGreet',
21
+ MUSIC = 'music',
22
+ NAME_CHANGE = 'nameChange',
23
+ OTHER = 'other',
24
+ OVERSIZED = 'oversized',
25
+ PET_IN_CABIN = 'petInCabin',
26
+ PET_IN_HOLD = 'petInHold',
27
+ SKIS = 'skis',
28
+ SPECIAL_MEAL = 'specialMeal',
29
+ STANDARD_MEAL = 'standardMeal',
30
+ }
@@ -0,0 +1,243 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { ref } from 'vue'
3
+
4
+ import CpAncillaryItem from '@/components/CpAncillaryItem.vue'
5
+
6
+ import { CpAncillaryItemTypes } from '@/constants/CpAncillaryItemTypes'
7
+ import { docCellStyle, docLabelStyle, docRowWrapStyle } from '@/stories/documentationStyles'
8
+
9
+ const galleryCellStyle = `${docCellStyle} width: 260px;`
10
+
11
+ const meta = {
12
+ title: 'Atoms/CpAncillaryItem',
13
+ component: CpAncillaryItem,
14
+ argTypes: {
15
+ currency: {
16
+ control: 'text',
17
+ description: 'ISO currency code used to format price',
18
+ },
19
+ description: {
20
+ control: 'text',
21
+ description: 'Description text for the ancillary',
22
+ },
23
+ locale: {
24
+ control: 'text',
25
+ description: 'Locale used to format price, defaults to the browser locale',
26
+ },
27
+ maxQuantity: {
28
+ control: 'number',
29
+ description: 'Maximum selectable quantity, null for uncapped',
30
+ },
31
+ maxQuantityLabel: {
32
+ control: 'text',
33
+ description: 'Caption shown next to the counter when maxQuantity is set',
34
+ },
35
+ price: {
36
+ control: 'number',
37
+ description: 'Price in minor units (cents), formatted with currency',
38
+ },
39
+ subtitle: {
40
+ control: 'text',
41
+ description: 'Optional second line rendered under the description',
42
+ },
43
+ title: {
44
+ control: 'text',
45
+ description: 'Title of the ancillary',
46
+ },
47
+ type: {
48
+ control: 'select',
49
+ options: Object.values(CpAncillaryItemTypes),
50
+ description: 'Picks the visual for the ancillary',
51
+ },
52
+ vertical: {
53
+ control: 'boolean',
54
+ description: 'Whether to use the column layout instead of the row layout',
55
+ },
56
+ },
57
+ } satisfies Meta<typeof CpAncillaryItem>
58
+
59
+ export default meta
60
+ type Story = StoryObj<typeof meta>
61
+
62
+ /**
63
+ * Default ancillary card. Use the controls to experiment with each prop in isolation.
64
+ */
65
+ export const Default: Story = {
66
+ args: {
67
+ title: 'Checked bag',
68
+ description: '23kg included',
69
+ type: CpAncillaryItemTypes.CHECKED_BAG,
70
+ quantity: 0,
71
+ maxQuantity: 3,
72
+ maxQuantityLabel: 'max. 3',
73
+ price: 2990,
74
+ currency: 'EUR',
75
+ vertical: false,
76
+ },
77
+ render: (args) => ({
78
+ components: { CpAncillaryItem },
79
+ setup() {
80
+ const quantity = ref(0)
81
+ return { args, quantity }
82
+ },
83
+ template: `
84
+ <div style="padding: 20px; min-width: 15rem;">
85
+ <CpAncillaryItem v-bind="args" v-model:quantity="quantity" />
86
+ </div>
87
+ `,
88
+ }),
89
+ }
90
+
91
+ /* -------------------------------------------------------------------------- */
92
+ /* States */
93
+ /* -------------------------------------------------------------------------- */
94
+
95
+ /**
96
+ * quantity > 0 adds the accent ring and swaps the counter background.
97
+ */
98
+ export const Selected: Story = {
99
+ args: {
100
+ ...Default.args,
101
+ quantity: 1,
102
+ },
103
+ render: (args) => ({
104
+ components: { CpAncillaryItem },
105
+ setup() {
106
+ const quantity = ref(1)
107
+ return { args, quantity }
108
+ },
109
+ template: `
110
+ <div style="padding: 20px; min-width: 15rem;">
111
+ <CpAncillaryItem v-bind="args" v-model:quantity="quantity" />
112
+ </div>
113
+ `,
114
+ }),
115
+ }
116
+
117
+ /**
118
+ * No maxQuantity: the counter has no upper bound and the caption is hidden.
119
+ */
120
+ export const Uncapped: Story = {
121
+ args: {
122
+ title: 'Extra weight',
123
+ description: 'Additional 5kg allowance',
124
+ type: CpAncillaryItemTypes.EXTRA_WEIGHT,
125
+ quantity: 0,
126
+ maxQuantity: null,
127
+ price: 990,
128
+ currency: 'EUR',
129
+ vertical: false,
130
+ },
131
+ render: (args) => ({
132
+ components: { CpAncillaryItem },
133
+ setup() {
134
+ const quantity = ref(0)
135
+ return { args, quantity }
136
+ },
137
+ template: `
138
+ <div style="padding: 20px; min-width: 15rem;">
139
+ <CpAncillaryItem v-bind="args" v-model:quantity="quantity" />
140
+ </div>
141
+ `,
142
+ }),
143
+ }
144
+
145
+ /**
146
+ * A type with no bundled visual falls back to the shopping-bag icon.
147
+ */
148
+ export const NoImage: Story = {
149
+ args: {
150
+ title: 'Cancellation insurance',
151
+ description: 'Refund protection for your trip',
152
+ type: CpAncillaryItemTypes.CANCELLATION_INSURANCE,
153
+ quantity: 0,
154
+ maxQuantity: 1,
155
+ maxQuantityLabel: 'max. 1',
156
+ price: 1490,
157
+ currency: 'EUR',
158
+ vertical: false,
159
+ },
160
+ render: (args) => ({
161
+ components: { CpAncillaryItem },
162
+ setup() {
163
+ const quantity = ref(0)
164
+ return { args, quantity }
165
+ },
166
+ template: `
167
+ <div style="padding: 20px; min-width: 15rem;">
168
+ <CpAncillaryItem v-bind="args" v-model:quantity="quantity" />
169
+ </div>
170
+ `,
171
+ }),
172
+ }
173
+
174
+ /**
175
+ * The column layout, used for baggage grids. subtitle renders a second details
176
+ * line under the description.
177
+ */
178
+ export const Vertical: Story = {
179
+ args: {
180
+ title: 'Cabin bag',
181
+ description: '10kg included',
182
+ subtitle: '9kg • 55 × 36 × 25 cm',
183
+ type: CpAncillaryItemTypes.CABIN_BAG,
184
+ quantity: 0,
185
+ maxQuantity: 1,
186
+ maxQuantityLabel: 'max. 1',
187
+ price: 1990,
188
+ currency: 'EUR',
189
+ vertical: true,
190
+ },
191
+ render: (args) => ({
192
+ components: { CpAncillaryItem },
193
+ setup() {
194
+ const quantity = ref(0)
195
+ return { args, quantity }
196
+ },
197
+ template: `
198
+ <div style="padding: 20px; min-width: 15rem;">
199
+ <CpAncillaryItem v-bind="args" v-model:quantity="quantity" />
200
+ </div>
201
+ `,
202
+ }),
203
+ }
204
+
205
+ /* -------------------------------------------------------------------------- */
206
+ /* Gallery */
207
+ /* -------------------------------------------------------------------------- */
208
+
209
+ /**
210
+ * Every enum value rendered at once: each real visual, and the fallback icon
211
+ * for the three art-less types.
212
+ */
213
+ export const Gallery: Story = {
214
+ args: {
215
+ ...Default.args,
216
+ },
217
+ parameters: { controls: { disable: true } },
218
+ render: () => ({
219
+ components: { CpAncillaryItem },
220
+ setup() {
221
+ const types = Object.values(CpAncillaryItemTypes)
222
+ const quantities = ref(Object.fromEntries(types.map((type) => [type, 0])))
223
+ return { types, quantities, galleryCellStyle, docLabelStyle, docRowWrapStyle }
224
+ },
225
+ template: `
226
+ <div :style="docRowWrapStyle">
227
+ <div v-for="type in types" :key="type" :style="galleryCellStyle">
228
+ <span :style="docLabelStyle">{{ type }}</span>
229
+ <CpAncillaryItem
230
+ v-model:quantity="quantities[type]"
231
+ :title="type"
232
+ description="Sample description"
233
+ :type="type"
234
+ :max-quantity="5"
235
+ max-quantity-label="max. 5"
236
+ :price="1000"
237
+ currency="EUR"
238
+ />
239
+ </div>
240
+ </div>
241
+ `,
242
+ }),
243
+ }