@citizenplane/pimp 18.23.6 → 18.25.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 (83) hide show
  1. package/dist/components/CpAncillaryItem.vue.d.ts.map +1 -1
  2. package/dist/components/CpDynamicMenu.vue.d.ts +51 -0
  3. package/dist/components/CpDynamicMenu.vue.d.ts.map +1 -0
  4. package/dist/components/CpIllustration.vue.d.ts +10 -0
  5. package/dist/components/CpIllustration.vue.d.ts.map +1 -0
  6. package/dist/components/CpLanguageSwitcher.vue.d.ts +35 -0
  7. package/dist/components/CpLanguageSwitcher.vue.d.ts.map +1 -0
  8. package/dist/components/index.d.ts +7 -1
  9. package/dist/components/index.d.ts.map +1 -1
  10. package/dist/constants/CpIllustrations.d.ts +55 -0
  11. package/dist/constants/CpIllustrations.d.ts.map +1 -0
  12. package/dist/constants/index.d.ts +2 -0
  13. package/dist/constants/index.d.ts.map +1 -1
  14. package/dist/pimp.es.js +2096 -1831
  15. package/dist/pimp.umd.js +45 -45
  16. package/dist/style.css +1 -1
  17. package/package.json +2 -1
  18. package/src/assets/illustrations/accessibility.webp +0 -0
  19. package/src/assets/illustrations/airplane-seat.webp +0 -0
  20. package/src/assets/illustrations/airport-experience.png +0 -0
  21. package/src/assets/illustrations/baggages.webp +0 -0
  22. package/src/assets/illustrations/boarding-pass-2.webp +0 -0
  23. package/src/assets/illustrations/boarding-pass.webp +0 -0
  24. package/src/assets/illustrations/boeing-787-dreamliner.webp +0 -0
  25. package/src/assets/illustrations/checkout.webp +0 -0
  26. package/src/assets/illustrations/clock.png +0 -0
  27. package/src/assets/illustrations/empty-basket.png +0 -0
  28. package/src/assets/illustrations/empty-result.webp +0 -0
  29. package/src/assets/illustrations/flip-clock.webp +0 -0
  30. package/src/assets/illustrations/frequent-flyer.webp +0 -0
  31. package/src/assets/illustrations/generic.webp +0 -0
  32. package/src/assets/illustrations/island.webp +0 -0
  33. package/src/assets/illustrations/miles.webp +0 -0
  34. package/src/assets/illustrations/name-change.png +0 -0
  35. package/src/assets/illustrations/passport.webp +0 -0
  36. package/src/assets/illustrations/payment-declined.png +0 -0
  37. package/src/assets/illustrations/pets.webp +0 -0
  38. package/src/assets/illustrations/plane.webp +0 -0
  39. package/src/assets/illustrations/sign-out.png +0 -0
  40. package/src/assets/illustrations/sport-equipments.webp +0 -0
  41. package/src/components/CpAncillaryCategory.vue +7 -7
  42. package/src/components/CpAncillaryItem.vue +25 -26
  43. package/src/components/CpDynamicMenu.vue +226 -0
  44. package/src/components/CpIllustration.vue +44 -0
  45. package/src/components/CpLanguageSwitcher.vue +151 -0
  46. package/src/components/index.ts +12 -0
  47. package/src/constants/CpIllustrations.ts +107 -0
  48. package/src/constants/index.ts +2 -0
  49. package/src/stories/CpDynamicMenu.stories.ts +279 -0
  50. package/src/stories/CpIllustration.stories.ts +129 -0
  51. package/src/stories/CpLanguageSwitcher.stories.ts +168 -0
  52. package/src/assets/images/ancillaries/checked-bag-size-2.png +0 -0
  53. package/src/assets/images/ancillary-categories/baggages.png +0 -0
  54. package/src/assets/images/ancillary-categories/name-change.png +0 -0
  55. package/src/assets/images/ancillary-categories/pets.png +0 -0
  56. package/src/assets/images/ancillary-categories/sport-equipments.png +0 -0
  57. /package/src/assets/{images/ancillaries → illustrations}/airport-transfer.png +0 -0
  58. /package/src/assets/{images/ancillaries → illustrations}/bicycle.png +0 -0
  59. /package/src/assets/{images/ancillaries → illustrations}/cabin-bag.png +0 -0
  60. /package/src/assets/{images/ancillary-categories → illustrations}/carbon-offset.png +0 -0
  61. /package/src/assets/{images/ancillaries → illustrations}/checked-bag-size-1.png +0 -0
  62. /package/src/assets/{images/ancillaries → illustrations}/checked-bag-size-3.png +0 -0
  63. /package/src/assets/{images/ancillaries → illustrations}/checked-bag.png +0 -0
  64. /package/src/assets/{images/ancillaries → illustrations}/connectivity.png +0 -0
  65. /package/src/assets/{images/ancillaries → illustrations}/diving-set.png +0 -0
  66. /package/src/assets/{images/ancillaries → illustrations}/extra-weight.png +0 -0
  67. /package/src/assets/{images/ancillaries → illustrations}/fast-track.png +0 -0
  68. /package/src/assets/{images/ancillaries → illustrations}/firearm.png +0 -0
  69. /package/src/assets/{images/ancillaries → illustrations}/fishing-rod.png +0 -0
  70. /package/src/assets/{images/ancillaries → illustrations}/golf.png +0 -0
  71. /package/src/assets/{images/ancillaries → illustrations}/hand-bag.png +0 -0
  72. /package/src/assets/{images/ancillary-categories → illustrations}/insurance.png +0 -0
  73. /package/src/assets/{images/ancillaries → illustrations}/kite-surf.png +0 -0
  74. /package/src/assets/{images/ancillaries → illustrations}/lounge-access.png +0 -0
  75. /package/src/assets/{images/ancillaries → illustrations}/meet-and-greet.png +0 -0
  76. /package/src/assets/{images/ancillaries → illustrations}/music.png +0 -0
  77. /package/src/assets/{images/ancillaries → illustrations}/oversized.png +0 -0
  78. /package/src/assets/{images/ancillaries → illustrations}/pet-in-cabin.png +0 -0
  79. /package/src/assets/{images/ancillaries → illustrations}/pet-in-hold.png +0 -0
  80. /package/src/assets/{images/ancillary-categories → illustrations}/restricted.png +0 -0
  81. /package/src/assets/{images/ancillaries → illustrations}/skis.png +0 -0
  82. /package/src/assets/{images/ancillaries → illustrations}/special-meal.png +0 -0
  83. /package/src/assets/{images/ancillaries → illustrations}/standard-meal.png +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@citizenplane/pimp",
3
- "version": "18.23.6",
3
+ "version": "18.25.0",
4
4
  "scripts": {
5
5
  "dev": "storybook dev -p 8081",
6
6
  "build-storybook": "storybook build --output-dir ./docs",
@@ -75,6 +75,7 @@
75
75
  "@vitest/coverage-v8": "4.1.9",
76
76
  "@vue/test-utils": "2.4.11",
77
77
  "commitlint-config-gitmoji": "2.3.1",
78
+ "flag-icons": "7.5.0",
78
79
  "happy-dom": "20.14.5",
79
80
  "husky": "9.1.7",
80
81
  "lint-staged": "17.2.0",
@@ -65,13 +65,13 @@ import CpIcon from '@/components/CpIcon.vue'
65
65
  import CpSwitch from '@/components/CpSwitch.vue'
66
66
  import CpTransitionExpand from '@/components/CpTransitionExpand.vue'
67
67
 
68
- import BaggagesVisual from '@/assets/images/ancillary-categories/baggages.png'
69
- import CarbonOffsetVisual from '@/assets/images/ancillary-categories/carbon-offset.png'
70
- import InsuranceVisual from '@/assets/images/ancillary-categories/insurance.png'
71
- import NameChangeVisual from '@/assets/images/ancillary-categories/name-change.png'
72
- import PetsVisual from '@/assets/images/ancillary-categories/pets.png'
73
- import RestrictedVisual from '@/assets/images/ancillary-categories/restricted.png'
74
- import SportEquipmentsVisual from '@/assets/images/ancillary-categories/sport-equipments.png'
68
+ import BaggagesVisual from '@/assets/illustrations/baggages.webp'
69
+ import CarbonOffsetVisual from '@/assets/illustrations/carbon-offset.png'
70
+ import InsuranceVisual from '@/assets/illustrations/insurance.png'
71
+ import NameChangeVisual from '@/assets/illustrations/name-change.png'
72
+ import PetsVisual from '@/assets/illustrations/pets.webp'
73
+ import RestrictedVisual from '@/assets/illustrations/restricted.png'
74
+ import SportEquipmentsVisual from '@/assets/illustrations/sport-equipments.webp'
75
75
  import { CpAncillaryCategoryTypes, isSwitchCategory } from '@/constants/CpAncillaryCategoryTypes'
76
76
 
77
77
  interface Props {
@@ -51,31 +51,30 @@ import CpButton from '@/components/CpButton.vue'
51
51
  import CpIcon from '@/components/CpIcon.vue'
52
52
  import CpQuantityCounter from '@/components/CpQuantityCounter.vue'
53
53
 
54
- import AirportTransferImage from '@/assets/images/ancillaries/airport-transfer.png'
55
- import BicycleImage from '@/assets/images/ancillaries/bicycle.png'
56
- import CabinBagImage from '@/assets/images/ancillaries/cabin-bag.png'
57
- import CheckedBagImage from '@/assets/images/ancillaries/checked-bag.png'
58
- import CheckedBagSize1Image from '@/assets/images/ancillaries/checked-bag-size-1.png'
59
- import CheckedBagSize2Image from '@/assets/images/ancillaries/checked-bag-size-2.png'
60
- import CheckedBagSize3Image from '@/assets/images/ancillaries/checked-bag-size-3.png'
61
- import ConnectivityImage from '@/assets/images/ancillaries/connectivity.png'
62
- import DivingSetImage from '@/assets/images/ancillaries/diving-set.png'
63
- import ExtraWeightImage from '@/assets/images/ancillaries/extra-weight.png'
64
- import FastTrackImage from '@/assets/images/ancillaries/fast-track.png'
65
- import FirearmImage from '@/assets/images/ancillaries/firearm.png'
66
- import FishingRodImage from '@/assets/images/ancillaries/fishing-rod.png'
67
- import GolfImage from '@/assets/images/ancillaries/golf.png'
68
- import HandBagImage from '@/assets/images/ancillaries/hand-bag.png'
69
- import KiteSurfImage from '@/assets/images/ancillaries/kite-surf.png'
70
- import LoungeAccessImage from '@/assets/images/ancillaries/lounge-access.png'
71
- import MeetAndGreetImage from '@/assets/images/ancillaries/meet-and-greet.png'
72
- import MusicImage from '@/assets/images/ancillaries/music.png'
73
- import OversizedImage from '@/assets/images/ancillaries/oversized.png'
74
- import PetInCabinImage from '@/assets/images/ancillaries/pet-in-cabin.png'
75
- import PetInHoldImage from '@/assets/images/ancillaries/pet-in-hold.png'
76
- import SkisImage from '@/assets/images/ancillaries/skis.png'
77
- import SpecialMealImage from '@/assets/images/ancillaries/special-meal.png'
78
- import StandardMealImage from '@/assets/images/ancillaries/standard-meal.png'
54
+ import AirportTransferImage from '@/assets/illustrations/airport-transfer.png'
55
+ import BicycleImage from '@/assets/illustrations/bicycle.png'
56
+ import CabinBagImage from '@/assets/illustrations/cabin-bag.png'
57
+ import CheckedBagImage from '@/assets/illustrations/checked-bag.png'
58
+ import CheckedBagSize1Image from '@/assets/illustrations/checked-bag-size-1.png'
59
+ import CheckedBagSize3Image from '@/assets/illustrations/checked-bag-size-3.png'
60
+ import ConnectivityImage from '@/assets/illustrations/connectivity.png'
61
+ import DivingSetImage from '@/assets/illustrations/diving-set.png'
62
+ import ExtraWeightImage from '@/assets/illustrations/extra-weight.png'
63
+ import FastTrackImage from '@/assets/illustrations/fast-track.png'
64
+ import FirearmImage from '@/assets/illustrations/firearm.png'
65
+ import FishingRodImage from '@/assets/illustrations/fishing-rod.png'
66
+ import GolfImage from '@/assets/illustrations/golf.png'
67
+ import HandBagImage from '@/assets/illustrations/hand-bag.png'
68
+ import KiteSurfImage from '@/assets/illustrations/kite-surf.png'
69
+ import LoungeAccessImage from '@/assets/illustrations/lounge-access.png'
70
+ import MeetAndGreetImage from '@/assets/illustrations/meet-and-greet.png'
71
+ import MusicImage from '@/assets/illustrations/music.png'
72
+ import OversizedImage from '@/assets/illustrations/oversized.png'
73
+ import PetInCabinImage from '@/assets/illustrations/pet-in-cabin.png'
74
+ import PetInHoldImage from '@/assets/illustrations/pet-in-hold.png'
75
+ import SkisImage from '@/assets/illustrations/skis.png'
76
+ import SpecialMealImage from '@/assets/illustrations/special-meal.png'
77
+ import StandardMealImage from '@/assets/illustrations/standard-meal.png'
79
78
  import { CpAncillaryItemTypes } from '@/constants/CpAncillaryItemTypes'
80
79
 
81
80
  interface Props {
@@ -114,7 +113,7 @@ const ancillaryImages: Partial<Record<CpAncillaryItemTypes, string>> = {
114
113
  [CpAncillaryItemTypes.CABIN_BAG]: CabinBagImage,
115
114
  [CpAncillaryItemTypes.CHECKED_BAG]: CheckedBagImage,
116
115
  [CpAncillaryItemTypes.CHECKED_BAG_SIZE_1]: CheckedBagSize1Image,
117
- [CpAncillaryItemTypes.CHECKED_BAG_SIZE_2]: CheckedBagSize2Image,
116
+ [CpAncillaryItemTypes.CHECKED_BAG_SIZE_2]: CheckedBagImage,
118
117
  [CpAncillaryItemTypes.CHECKED_BAG_SIZE_3]: CheckedBagSize3Image,
119
118
  [CpAncillaryItemTypes.CONNECTIVITY]: ConnectivityImage,
120
119
  [CpAncillaryItemTypes.DIVING_SET]: DivingSetImage,
@@ -0,0 +1,226 @@
1
+ <template>
2
+ <div class="cpDynamicMenu">
3
+ <cp-menu v-if="!isMobile" ref="menuRef" v-bind="menuProps" @hide="onMenuHide" @show="onMenuShow">
4
+ <template #trigger>
5
+ <slot name="trigger" />
6
+ </template>
7
+ <slot />
8
+ </cp-menu>
9
+ <template v-else>
10
+ <div
11
+ ref="triggerRef"
12
+ :aria-controls="menuId"
13
+ :aria-expanded="isSheetVisible"
14
+ :aria-haspopup="popupType"
15
+ class="cpDynamicMenu__trigger"
16
+ @click="toggleSheet"
17
+ >
18
+ <slot name="trigger" />
19
+ </div>
20
+ <cp-bottom-sheet v-model="isSheetVisible" v-bind="bottomSheetProps">
21
+ <template v-if="hasSheetTitle" #header>
22
+ <slot name="sheet-title">
23
+ <cp-heading class="cpDynamicMenu__sheetTitle" heading-level="h3">{{ sheetTitle }}</cp-heading>
24
+ </slot>
25
+ </template>
26
+ <cp-menu-list v-if="hasItems" :id="menuId" :items="items" @async-complete="onItemClick" @click="onItemClick" />
27
+ <slot v-else />
28
+ <template #footer>
29
+ <slot name="footer" />
30
+ </template>
31
+ </cp-bottom-sheet>
32
+ </template>
33
+ </div>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import type { MenuItem } from 'primevue/menuitem'
38
+ import { computed, ref, useId, useSlots, useTemplateRef, watch } from 'vue'
39
+
40
+ import type { CpMenuPlacement } from '@/components/CpMenu.vue'
41
+
42
+ import { useIsBreakpoint } from '@/composables/useIsBreakpoint'
43
+
44
+ import CpBottomSheet from '@/components/CpBottomSheet.vue'
45
+ import CpHeading from '@/components/CpHeading.vue'
46
+ import CpMenu from '@/components/CpMenu.vue'
47
+ import CpMenuList from '@/components/CpMenuList.vue'
48
+
49
+ import { Breakpoints } from '@/constants/layout'
50
+ import { getKeyboardFocusableElements } from '@/helpers/dom'
51
+
52
+ interface Props {
53
+ animationDuration?: number
54
+ breakpoint?: number
55
+ class?: string
56
+ contentClass?: string
57
+ expandOnContentDrag?: boolean
58
+ halfAndExpand?: boolean
59
+ items?: MenuItem[]
60
+ keepOpenOnClick?: boolean
61
+ placement?: CpMenuPlacement
62
+ preventClose?: boolean
63
+ sheetTitle?: string
64
+ snapPoints?: Array<number | `${number}%`>
65
+ swipeCloseThreshold?: string
66
+ }
67
+
68
+ const props = defineProps<Props>()
69
+
70
+ const emits = defineEmits<{
71
+ hide: []
72
+ show: []
73
+ }>()
74
+
75
+ const slots = useSlots()
76
+
77
+ const isMobile = useIsBreakpoint(props.breakpoint ?? Breakpoints.MOBILE)
78
+
79
+ const menuRef = useTemplateRef('menuRef')
80
+ const triggerRef = useTemplateRef<HTMLElement>('triggerRef')
81
+
82
+ const isMenuOpen = ref(false)
83
+ const isSheetVisible = ref(false)
84
+ const menuId = useId()
85
+
86
+ const hasItems = computed(() => Boolean(props.items?.length))
87
+ const hasSheetTitle = computed(() => Boolean(props.sheetTitle || slots['sheet-title']))
88
+
89
+ const popupType = computed(() => {
90
+ if (hasItems.value) return 'menu'
91
+ return 'dialog'
92
+ })
93
+
94
+ const menuProps = computed(() => ({
95
+ class: props.class,
96
+ forcePopover: true,
97
+ items: props.items,
98
+ keepOpenOnClick: props.keepOpenOnClick,
99
+ placement: props.placement,
100
+ }))
101
+
102
+ const sheetContentClass = computed(() => {
103
+ if (!hasItems.value) return props.contentClass
104
+ if (!props.contentClass) return 'cpDynamicMenu__menuListContent'
105
+ return `cpDynamicMenu__menuListContent ${props.contentClass}`
106
+ })
107
+
108
+ const sheetBaseClass = computed(() => {
109
+ if (hasSheetTitle.value) return 'cpDynamicMenu__sheet'
110
+ return 'cpDynamicMenu__sheet cpDynamicMenu__sheet--hasNoTitle'
111
+ })
112
+
113
+ const sheetClass = computed(() => {
114
+ if (!props.class) return sheetBaseClass.value
115
+ return `${sheetBaseClass.value} ${props.class}`
116
+ })
117
+
118
+ const bottomSheetProps = computed(() => ({
119
+ animationDuration: props.animationDuration,
120
+ canBackdropClose: !props.preventClose,
121
+ canSwipeClose: !props.preventClose,
122
+ class: sheetClass.value,
123
+ contentClass: sheetContentClass.value,
124
+ expandOnContentDrag: props.expandOnContentDrag,
125
+ halfAndExpand: props.halfAndExpand,
126
+ preventClose: props.preventClose,
127
+ snapPoints: props.snapPoints,
128
+ swipeCloseThreshold: props.swipeCloseThreshold,
129
+ }))
130
+
131
+ const focusTrigger = () => {
132
+ if (!triggerRef.value) return
133
+ const [firstFocusableElement] = getKeyboardFocusableElements(triggerRef.value) as HTMLElement[]
134
+ firstFocusableElement?.focus()
135
+ }
136
+
137
+ // CpMenu emits show/hide twice when driven imperatively, so only real state changes are relayed.
138
+ const onMenuShow = () => {
139
+ if (isMenuOpen.value) return
140
+ isMenuOpen.value = true
141
+ emits('show')
142
+ }
143
+
144
+ const onMenuHide = () => {
145
+ if (!isMenuOpen.value) return
146
+ isMenuOpen.value = false
147
+ emits('hide')
148
+ }
149
+
150
+ const toggleSheet = () => {
151
+ isSheetVisible.value = !isSheetVisible.value
152
+ }
153
+
154
+ const onItemClick = () => {
155
+ if (props.keepOpenOnClick) return
156
+ isSheetVisible.value = false
157
+ }
158
+
159
+ const show = (event: Event) => {
160
+ if (isMobile.value) {
161
+ isSheetVisible.value = true
162
+ return
163
+ }
164
+ menuRef.value?.show(event)
165
+ }
166
+
167
+ const hide = () => {
168
+ if (isMobile.value) {
169
+ isSheetVisible.value = false
170
+ return
171
+ }
172
+ menuRef.value?.hide()
173
+ }
174
+
175
+ const toggle = (event: Event) => {
176
+ if (isMobile.value) {
177
+ toggleSheet()
178
+ return
179
+ }
180
+ menuRef.value?.toggle(event)
181
+ }
182
+
183
+ watch(isSheetVisible, (isVisible) => {
184
+ if (isVisible) {
185
+ emits('show')
186
+ return
187
+ }
188
+ emits('hide')
189
+ focusTrigger()
190
+ })
191
+
192
+ watch(isMobile, (isMobileViewport) => {
193
+ if (!isMobileViewport) {
194
+ isSheetVisible.value = false
195
+ return
196
+ }
197
+ if (isMenuOpen.value) menuRef.value?.hide()
198
+ })
199
+
200
+ defineExpose({ show, hide, toggle })
201
+ </script>
202
+
203
+ <style lang="scss">
204
+ .cpDynamicMenu {
205
+ display: contents;
206
+
207
+ &__trigger {
208
+ display: inline-flex;
209
+ cursor: pointer;
210
+ }
211
+
212
+ &__sheetTitle {
213
+ font-size: var(--cp-text-size-2xl);
214
+ font-weight: 700;
215
+ line-height: var(--cp-line-height-2xl);
216
+ }
217
+
218
+ &__menuListContent.cpBottomSheet__content {
219
+ padding: var(--cp-spacing-sm) 0;
220
+ }
221
+
222
+ &__sheet--hasNoTitle .cpBottomSheet__header {
223
+ padding-bottom: 0;
224
+ }
225
+ }
226
+ </style>
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <img
3
+ alt=""
4
+ aria-hidden="true"
5
+ class="cpIllustration"
6
+ :class="nameClass"
7
+ :height="size"
8
+ :src="source"
9
+ :width="size"
10
+ />
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { computed } from 'vue'
15
+
16
+ import { type CpIllustrationName, CpIllustrations } from '@/constants'
17
+
18
+ export interface Props {
19
+ name: string
20
+ size?: number
21
+ }
22
+
23
+ const props = withDefaults(defineProps<Props>(), {
24
+ size: 24,
25
+ })
26
+
27
+ const resolvedName = computed<CpIllustrationName>(() => {
28
+ const name = props.name as CpIllustrationName
29
+
30
+ if (!CpIllustrations[name]) return 'generic'
31
+
32
+ return name
33
+ })
34
+
35
+ const source = computed(() => CpIllustrations[resolvedName.value])
36
+
37
+ const nameClass = computed(() => `cpIllustration--${resolvedName.value}`)
38
+ </script>
39
+
40
+ <style lang="scss">
41
+ .cpIllustration {
42
+ object-fit: contain;
43
+ }
44
+ </style>
@@ -0,0 +1,151 @@
1
+ <template>
2
+ <cp-dynamic-menu
3
+ ref="dynamicMenuRef"
4
+ :breakpoint="breakpoint"
5
+ :class="props.class"
6
+ content-class="cpLanguageSwitcher__content"
7
+ :placement="placement"
8
+ >
9
+ <template #trigger>
10
+ <slot name="trigger">
11
+ <cp-button appearance="tertiary" color="accent" size="sm">
12
+ <template #leading-icon>
13
+ <cp-icon type="globe" />
14
+ </template>
15
+ {{ uppercasedCurrentCode }}
16
+ </cp-button>
17
+ </slot>
18
+ </template>
19
+ <ul class="cpLanguageSwitcher__list">
20
+ <li v-for="lang in langs" :key="lang.code">
21
+ <button
22
+ :aria-current="getAriaCurrent(lang.code)"
23
+ class="cpLanguageSwitcher__lang"
24
+ :class="getLangClass(lang.code)"
25
+ type="button"
26
+ @click="selectLanguage(lang.code)"
27
+ >
28
+ <span class="cpLanguageSwitcher__left">
29
+ <span v-if="lang.flag" class="cpLanguageSwitcher__flag" :class="getFlagClass(lang.flag)" />
30
+ <span class="cpLanguageSwitcher__name">{{ lang.name }}</span>
31
+ </span>
32
+ <cp-icon v-if="isCurrentLanguage(lang.code)" class="cpLanguageSwitcher__check" size="20" type="check" />
33
+ </button>
34
+ </li>
35
+ </ul>
36
+ </cp-dynamic-menu>
37
+ </template>
38
+
39
+ <script setup lang="ts">
40
+ import { computed, useTemplateRef } from 'vue'
41
+
42
+ import type { CpMenuPlacement } from '@/components/CpMenu.vue'
43
+
44
+ import CpButton from '@/components/CpButton.vue'
45
+ import CpDynamicMenu from '@/components/CpDynamicMenu.vue'
46
+ import CpIcon from '@/components/CpIcon.vue'
47
+
48
+ export interface LanguageOption {
49
+ code: string
50
+ flag?: string
51
+ name: string
52
+ }
53
+
54
+ interface Props {
55
+ breakpoint?: number
56
+ class?: string
57
+ langs: LanguageOption[]
58
+ placement?: CpMenuPlacement
59
+ }
60
+
61
+ const props = defineProps<Props>()
62
+
63
+ const currentCode = defineModel<string>()
64
+
65
+ const dynamicMenuRef = useTemplateRef('dynamicMenuRef')
66
+
67
+ const uppercasedCurrentCode = computed(() => {
68
+ if (!currentCode.value) return ''
69
+ return currentCode.value.toUpperCase()
70
+ })
71
+
72
+ const isCurrentLanguage = (code: string) => code === currentCode.value
73
+
74
+ const getLangClass = (code: string) => ({ 'cpLanguageSwitcher__lang--isSelected': isCurrentLanguage(code) })
75
+
76
+ // flag-icons is a devDependency only: the consumer app loads flag-icons.min.css itself.
77
+ const getFlagClass = (flag: string) => `fi fi-${flag.toLowerCase()}`
78
+
79
+ const getAriaCurrent = (code: string) => {
80
+ if (!isCurrentLanguage(code)) return undefined
81
+ return 'true'
82
+ }
83
+
84
+ const selectLanguage = (code: string) => {
85
+ if (!isCurrentLanguage(code)) currentCode.value = code
86
+ dynamicMenuRef.value?.hide()
87
+ }
88
+ </script>
89
+
90
+ <style lang="scss">
91
+ .cpLanguageSwitcher {
92
+ &__content.cpBottomSheet__content {
93
+ padding: var(--cp-spacing-sm) 0;
94
+ }
95
+
96
+ &__list {
97
+ display: flex;
98
+ flex-direction: column;
99
+ padding: 0 var(--cp-spacing-sm);
100
+ margin: 0;
101
+ gap: var(--cp-spacing-sm);
102
+ list-style: none;
103
+ }
104
+
105
+ &__lang {
106
+ @extend %u-focus-outline;
107
+
108
+ display: flex;
109
+ width: 100%;
110
+ align-items: center;
111
+ justify-content: space-between;
112
+ padding: var(--cp-spacing-lg) var(--cp-spacing-xl) var(--cp-spacing-lg) var(--cp-spacing-lg);
113
+ border: none;
114
+ border-radius: var(--cp-radius-md);
115
+ background-color: transparent;
116
+ color: var(--cp-text-primary);
117
+ cursor: pointer;
118
+ gap: var(--cp-spacing-md);
119
+
120
+ &:hover {
121
+ background-color: var(--cp-background-primary-hover);
122
+ }
123
+
124
+ &--isSelected {
125
+ background-color: var(--cp-background-accent-primary);
126
+ }
127
+ }
128
+
129
+ &__left {
130
+ display: flex;
131
+ align-items: center;
132
+ gap: var(--cp-spacing-lg);
133
+ }
134
+
135
+ &__flag {
136
+ flex: none;
137
+ border-radius: var(--cp-radius-sm);
138
+ }
139
+
140
+ &__name {
141
+ font-size: var(--cp-text-size-md);
142
+ font-weight: 500;
143
+ line-height: var(--cp-line-height-md);
144
+ }
145
+
146
+ &__check {
147
+ flex: none;
148
+ color: var(--cp-text-accent-primary);
149
+ }
150
+ }
151
+ </style>
@@ -36,11 +36,14 @@ import CpDate from './CpDate.vue'
36
36
  import CpDatepicker from './CpDatepicker.vue'
37
37
  import CpDialog from './CpDialog.vue'
38
38
  import CpDynamicDialog from './CpDynamicDialog.vue'
39
+ import CpDynamicMenu from './CpDynamicMenu.vue'
39
40
  import CpFlightTabs from './CpFlightTabs.vue'
40
41
  import CpHeading from './CpHeading.vue'
41
42
  import CpIcon from './CpIcon.vue'
43
+ import CpIllustration from './CpIllustration.vue'
42
44
  import CpInput from './CpInput.vue'
43
45
  import CpItemActions from './CpItemActions.vue'
46
+ import CpLanguageSwitcher from './CpLanguageSwitcher.vue'
44
47
  import CpLoader from './CpLoader.vue'
45
48
  import CpMenu from './CpMenu.vue'
46
49
  import CpMenuItem from './CpMenuItem.vue'
@@ -111,11 +114,13 @@ const Components = {
111
114
  CpButtonGroup,
112
115
  CpDialog,
113
116
  CpDynamicDialog,
117
+ CpDynamicMenu,
114
118
  CpDate,
115
119
  CpContextualMenu,
116
120
  CpMenu,
117
121
  CpMenuItem,
118
122
  CpMenuList,
123
+ CpLanguageSwitcher,
119
124
  CpItemActions,
120
125
  CpCoreDatepicker,
121
126
  CpDatepicker,
@@ -146,6 +151,7 @@ const Components = {
146
151
  CpTable,
147
152
  CpTableColumnEditor,
148
153
  CpIcon,
154
+ CpIllustration,
149
155
  CpTelInput,
150
156
  CpTooltip,
151
157
  CpTextMorph,
@@ -201,9 +207,11 @@ const Pimp: Plugin = {
201
207
  },
202
208
  }
203
209
 
210
+ export type { LanguageOption } from '@/components/CpLanguageSwitcher.vue'
204
211
  export type { CpAncillaryFlightSelectionFlight } from '@/constants/CpAncillaryFlightSelectionTypes'
205
212
  export type { CpBasketCategory, CpBasketGroup, CpBasketRow } from '@/constants/CpBasketTypes'
206
213
  export type { CpFlightTabsFlight } from '@/constants/CpFlightTabsTypes'
214
+ export type { CpIllustrationName } from '@/constants/CpIllustrations'
207
215
  export type { CpStepperStep } from '@/constants/CpStepperTypes'
208
216
  export type {
209
217
  CpSeatMapConfig,
@@ -228,6 +236,7 @@ export type {
228
236
 
229
237
  export { CpAncillaryCategoryTypes, isSwitchCategory } from '@/constants/CpAncillaryCategoryTypes'
230
238
  export { CpAncillaryItemTypes } from '@/constants/CpAncillaryItemTypes'
239
+ export { CpIllustrationNames, CpIllustrations } from '@/constants/CpIllustrations'
231
240
  export {
232
241
  CpSeatMapTransitionDirections,
233
242
  DEFAULT_CP_SEAT_MAP_CONFIG,
@@ -262,11 +271,14 @@ export {
262
271
  CpDatepicker,
263
272
  CpDialog,
264
273
  CpDynamicDialog,
274
+ CpDynamicMenu,
265
275
  CpFlightTabs,
266
276
  CpHeading,
267
277
  CpIcon,
278
+ CpIllustration,
268
279
  CpInput,
269
280
  CpItemActions,
281
+ CpLanguageSwitcher,
270
282
  CpLoader,
271
283
  CpMenu,
272
284
  CpMenuItem,