@citizenplane/pimp 18.6.2 → 18.8.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 (159) hide show
  1. package/biome.json +178 -0
  2. package/dist/components/BaseInputLabel.vue.d.ts.map +1 -1
  3. package/dist/components/BaseSelectClearButton.vue.d.ts.map +1 -1
  4. package/dist/components/CpAccordion.vue.d.ts.map +1 -1
  5. package/dist/components/CpAlert.vue.d.ts.map +1 -1
  6. package/dist/components/CpBadge.vue.d.ts.map +1 -1
  7. package/dist/components/CpButton.vue.d.ts.map +1 -1
  8. package/dist/components/CpButtonToggle.vue.d.ts +1 -1
  9. package/dist/components/CpButtonToggle.vue.d.ts.map +1 -1
  10. package/dist/components/CpCalendar.vue.d.ts.map +1 -1
  11. package/dist/components/CpCheckbox.vue.d.ts.map +1 -1
  12. package/dist/components/CpContextualMenu.vue.d.ts.map +1 -1
  13. package/dist/components/CpDate.vue.d.ts.map +1 -1
  14. package/dist/components/CpDatepicker.vue.d.ts.map +1 -1
  15. package/dist/components/CpInput.vue.d.ts.map +1 -1
  16. package/dist/components/CpItemActions.vue.d.ts +2 -0
  17. package/dist/components/CpItemActions.vue.d.ts.map +1 -1
  18. package/dist/components/CpMenu.vue.d.ts.map +1 -1
  19. package/dist/components/CpMenuItem.vue.d.ts.map +1 -1
  20. package/dist/components/CpMultiselect.vue.d.ts.map +1 -1
  21. package/dist/components/CpRadio.vue.d.ts.map +1 -1
  22. package/dist/components/CpRadioGroup.vue.d.ts.map +1 -1
  23. package/dist/components/CpRadioNew.vue.d.ts.map +1 -1
  24. package/dist/components/CpSelect.vue.d.ts.map +1 -1
  25. package/dist/components/CpSelectMenu.vue.d.ts.map +1 -1
  26. package/dist/components/CpSelectableButton.vue.d.ts +1 -1
  27. package/dist/components/CpSelectableButton.vue.d.ts.map +1 -1
  28. package/dist/components/CpSwitch.vue.d.ts.map +1 -1
  29. package/dist/components/CpTable.vue.d.ts.map +1 -1
  30. package/dist/components/CpTelInput.vue.d.ts.map +1 -1
  31. package/dist/components/CpTooltip.vue.d.ts +2 -0
  32. package/dist/components/CpTooltip.vue.d.ts.map +1 -1
  33. package/dist/components/CpTrip.vue.d.ts +48 -0
  34. package/dist/components/CpTrip.vue.d.ts.map +1 -0
  35. package/dist/components/CpTripTimeline.vue.d.ts +24 -0
  36. package/dist/components/CpTripTimeline.vue.d.ts.map +1 -0
  37. package/dist/components/index.d.ts +2 -1
  38. package/dist/components/index.d.ts.map +1 -1
  39. package/dist/composables/useDynamicSize.d.ts +9 -0
  40. package/dist/composables/useDynamicSize.d.ts.map +1 -0
  41. package/dist/constants/index.d.ts +2 -2
  42. package/dist/constants/index.d.ts.map +1 -1
  43. package/dist/constants/layout/Breakpoints.d.ts +9 -0
  44. package/dist/constants/layout/Breakpoints.d.ts.map +1 -0
  45. package/dist/constants/layout/Sizes.d.ts +2 -0
  46. package/dist/constants/layout/Sizes.d.ts.map +1 -0
  47. package/dist/constants/layout/index.d.ts +3 -0
  48. package/dist/constants/layout/index.d.ts.map +1 -0
  49. package/dist/helpers/functions.d.ts +1 -0
  50. package/dist/helpers/functions.d.ts.map +1 -1
  51. package/dist/libs/CoreDatepicker.vue.d.ts.map +1 -1
  52. package/dist/pimp.es.js +6797 -6461
  53. package/dist/pimp.umd.js +54 -54
  54. package/dist/style.css +1 -1
  55. package/package.json +14 -24
  56. package/src/assets/css/base.css +17 -11
  57. package/src/assets/css/colors.css +12 -22
  58. package/src/assets/css/dimensions.css +4 -0
  59. package/src/assets/css/shadows.css +0 -3
  60. package/src/assets/css/tokens.css +21 -65
  61. package/src/assets/css/typography.css +0 -17
  62. package/src/assets/main.css +7 -7
  63. package/src/assets/styles/helpers/_functions.scss +2 -2
  64. package/src/assets/styles/utilities/_index.scss +2 -3
  65. package/src/components/BaseInputLabel.vue +7 -11
  66. package/src/components/BaseSelectClearButton.vue +6 -7
  67. package/src/components/CpAccordion.vue +27 -28
  68. package/src/components/CpAccordionGroup.vue +2 -2
  69. package/src/components/CpAlert.vue +12 -11
  70. package/src/components/CpBadge.vue +4 -19
  71. package/src/components/CpButton.vue +23 -25
  72. package/src/components/CpButtonGroup.vue +2 -2
  73. package/src/components/CpButtonToggle.vue +22 -22
  74. package/src/components/CpCalendar.vue +30 -26
  75. package/src/components/CpCheckbox.vue +29 -33
  76. package/src/components/CpContextualMenu.vue +1 -2
  77. package/src/components/CpDate.vue +72 -76
  78. package/src/components/CpDatepicker.vue +2 -3
  79. package/src/components/CpDialog.vue +8 -8
  80. package/src/components/CpHeading.vue +6 -6
  81. package/src/components/CpIcon.vue +2 -2
  82. package/src/components/CpInput.vue +46 -48
  83. package/src/components/CpItemActions.vue +17 -16
  84. package/src/components/CpMenu.vue +8 -9
  85. package/src/components/CpMenuItem.vue +7 -7
  86. package/src/components/CpMenuList.vue +3 -3
  87. package/src/components/CpMultiselect.vue +29 -30
  88. package/src/components/CpRadio.vue +53 -59
  89. package/src/components/CpRadioGroup.vue +11 -12
  90. package/src/components/CpRadioNew.vue +56 -58
  91. package/src/components/CpSelect.vue +42 -42
  92. package/src/components/CpSelectMenu.vue +32 -32
  93. package/src/components/CpSelectableButton.vue +50 -51
  94. package/src/components/CpSwitch.vue +43 -44
  95. package/src/components/CpTable.vue +69 -81
  96. package/src/components/CpTableColumnEditor.vue +16 -16
  97. package/src/components/CpTableEmptyState.vue +4 -4
  98. package/src/components/CpTableFooter.vue +2 -2
  99. package/src/components/CpTableFooterDesktop.vue +2 -2
  100. package/src/components/CpTableFooterDetails.vue +2 -2
  101. package/src/components/CpTableFooterMobile.vue +4 -4
  102. package/src/components/CpTabs.vue +1 -1
  103. package/src/components/CpTelInput.vue +31 -32
  104. package/src/components/CpTextarea.vue +13 -13
  105. package/src/components/CpToast.vue +25 -24
  106. package/src/components/CpTooltip.vue +15 -13
  107. package/src/components/CpTransitionCounter.vue +1 -1
  108. package/src/components/CpTransitionExpand.vue +5 -5
  109. package/src/components/CpTransitionSize.vue +1 -1
  110. package/src/components/CpTrip.vue +190 -0
  111. package/src/components/CpTripTimeline.vue +272 -0
  112. package/src/components/index.ts +36 -34
  113. package/src/composables/useDynamicSize.ts +60 -0
  114. package/src/constants/index.ts +2 -2
  115. package/src/constants/layout/Breakpoints.ts +8 -0
  116. package/src/constants/layout/Sizes.ts +1 -0
  117. package/src/constants/layout/index.ts +3 -0
  118. package/src/directives/ClickOutside.ts +1 -1
  119. package/src/directives/ResizeSelect.ts +1 -1
  120. package/src/helpers/functions.ts +1 -1
  121. package/src/helpers/index.ts +1 -1
  122. package/src/libs/CoreDatepicker.vue +115 -134
  123. package/src/stories/Colors.stories.ts +2 -1
  124. package/src/stories/CpAccordion.stories.ts +2 -2
  125. package/src/stories/CpAccordionGroup.stories.ts +1 -2
  126. package/src/stories/CpButtonToggle.stories.ts +1 -2
  127. package/src/stories/CpCheckbox.stories.ts +1 -2
  128. package/src/stories/CpContextualMenu.stories.ts +1 -2
  129. package/src/stories/CpDate.stories.ts +1 -2
  130. package/src/stories/CpDatepicker.stories.ts +1 -2
  131. package/src/stories/CpDialog.stories.ts +1 -2
  132. package/src/stories/CpInput.stories.ts +1 -2
  133. package/src/stories/CpItemActions.stories.ts +10 -5
  134. package/src/stories/CpMenu.stories.ts +1 -2
  135. package/src/stories/CpMenuItem.stories.ts +1 -2
  136. package/src/stories/CpMultiselect.stories.ts +1 -2
  137. package/src/stories/CpRadio.stories.ts +1 -2
  138. package/src/stories/CpRadioGroup.stories.ts +1 -2
  139. package/src/stories/CpSelect.stories.ts +1 -2
  140. package/src/stories/CpSelectMenu.stories.ts +1 -2
  141. package/src/stories/CpSwitch.stories.ts +1 -2
  142. package/src/stories/CpTable.stories.ts +2 -3
  143. package/src/stories/CpTabs.stories.ts +1 -2
  144. package/src/stories/CpText.stories.ts +2 -1
  145. package/src/stories/CpTextarea.stories.ts +1 -2
  146. package/src/stories/CpToast.stories.ts +2 -3
  147. package/src/stories/CpTransitionCounter.stories.ts +1 -2
  148. package/src/stories/CpTransitionExpand.stories.ts +1 -2
  149. package/src/stories/CpTransitionListItems.stories.ts +1 -2
  150. package/src/stories/CpTransitionSize.stories.ts +1 -2
  151. package/src/stories/CpTransitionSlide.stories.ts +1 -2
  152. package/src/stories/CpTransitionTabContent.stories.ts +1 -2
  153. package/src/stories/CpTrip.stories.ts +323 -0
  154. package/src/stories/Dimensions.stories.ts +1 -0
  155. package/src/stories/Shadows.stories.ts +1 -0
  156. package/src/stories/Typography.stories.ts +1 -0
  157. package/src/vendors/ff-polyfill.ts +1 -1
  158. package/vitest.workspace.js +1 -1
  159. package/src/components/Pimp.vue +0 -10
@@ -1,12 +1,11 @@
1
1
  <template>
2
2
  <v-tooltip
3
3
  :aria-id="ariaId"
4
- class="cpTooltip"
5
- :class="dynamicTooltipClasses"
6
- :container="false"
4
+ :popper-class="dynamicTooltipClasses"
7
5
  :disabled
8
6
  :distance
9
7
  :placement
8
+ :container="dynamicContainer"
10
9
  >
11
10
  <slot />
12
11
  <template #popper>
@@ -34,10 +33,10 @@
34
33
  </template>
35
34
 
36
35
  <script setup lang="ts">
36
+ // biome-ignore lint: required for floating-vue
37
37
  import { Tooltip as VTooltip } from 'floating-vue'
38
- import { useId, useSlots, computed } from 'vue'
39
-
40
38
  import type { Slots } from 'vue'
39
+ import { computed, useId, useSlots } from 'vue'
41
40
 
42
41
  import { Colors } from '@/constants'
43
42
  import { capitalizeFirstLetter } from '@/helpers'
@@ -46,6 +45,7 @@ type TooltipColors = Extract<Colors, 'accent' | 'neutral'>
46
45
  type Placement = 'top' | 'right' | 'bottom' | 'left'
47
46
 
48
47
  interface Props {
48
+ allowOutsideContainer?: boolean
49
49
  color?: TooltipColors
50
50
  content?: string
51
51
  disabled?: boolean
@@ -60,6 +60,7 @@ const props = withDefaults(defineProps<Props>(), {
60
60
  color: 'accent',
61
61
  placement: undefined,
62
62
  subcontent: '',
63
+ allowOutsideContainer: false,
63
64
  })
64
65
 
65
66
  const ariaId = useId()
@@ -67,7 +68,8 @@ const slots: Slots = useSlots()
67
68
 
68
69
  const hasSubcontent = computed(() => !!(props.subcontent || slots.subcontent))
69
70
 
70
- const dynamicTooltipClasses = computed(() => [`cpTooltip--is${capitalizeFirstLetter(props.color)}`])
71
+ const dynamicTooltipClasses = computed(() => ['cpTooltip', `cpTooltip--is${capitalizeFirstLetter(props.color)}`])
72
+ const dynamicContainer = computed(() => props.allowOutsideContainer ? undefined : false)
71
73
  </script>
72
74
 
73
75
  <style lang="scss">
@@ -76,14 +78,14 @@ const dynamicTooltipClasses = computed(() => [`cpTooltip--is${capitalizeFirstLet
76
78
  white-space: pre-wrap;
77
79
 
78
80
  .v-popper__inner {
81
+ max-width: fn.px-to-rem(300);
79
82
  padding: var(--cp-spacing-md) var(--cp-spacing-lg);
80
83
  border-radius: var(--cp-radius-md);
81
84
  background-color: var(--cp-tooltip-bg);
82
85
  font-size: var(--cp-text-size-xs);
86
+ font-weight: 400;
83
87
  line-height: var(--cp-line-height-xs);
84
- max-width: fn.px-to-rem(300);
85
88
  text-align: left;
86
- font-weight: 400;
87
89
  }
88
90
 
89
91
  .cpTooltip__content {
@@ -97,16 +99,16 @@ const dynamicTooltipClasses = computed(() => [`cpTooltip--is${capitalizeFirstLet
97
99
  }
98
100
 
99
101
  .cpTooltip__separator {
102
+ width: 100%;
100
103
  border: none;
101
104
  border-top: 1px solid var(--cp-colors-white);
102
- opacity: 0.5;
103
- width: 100%;
104
105
  margin: var(--cp-spacing-md) 0;
106
+ opacity: 0.5;
105
107
  }
106
108
 
107
109
  .cpTooltip__subcontent {
108
- font-style: italic;
109
110
  color: var(--cp-tooltip-subcontent-text);
111
+ font-style: italic;
110
112
  }
111
113
 
112
114
  .v-popper__arrow-outer {
@@ -120,9 +122,9 @@ const dynamicTooltipClasses = computed(() => [`cpTooltip--is${capitalizeFirstLet
120
122
  }
121
123
 
122
124
  &.cpTooltip--isNeutral {
123
- --cp-tooltip-bg: color-mix(in srgb, var(--cp-background-black) 80%, transparent);
125
+ --cp-tooltip-bg: color-mix(in sRGB, var(--cp-background-black) 80%, transparent);
124
126
  --cp-tooltip-content-text: var(--cp-foreground-white);
125
- --cp-tooltip-subcontent-text: color-mix(in srgb, var(--cp-foreground-white) 70%, transparent);
127
+ --cp-tooltip-subcontent-text: color-mix(in sRGB, var(--cp-foreground-white) 70%, transparent);
126
128
  }
127
129
  }
128
130
  </style>
@@ -9,7 +9,7 @@
9
9
  </template>
10
10
 
11
11
  <script setup lang="ts">
12
- import { ref, computed, watch } from 'vue'
12
+ import { computed, ref, watch } from 'vue'
13
13
 
14
14
  interface Props {
15
15
  counterNumber?: number
@@ -29,7 +29,7 @@ const enter = (element: Element) => {
29
29
  // Force repaint to make sure the
30
30
  // animation is triggered correctly.
31
31
 
32
- // @typescript-eslint/no-unused-expressions
32
+ // Intentional expression statement to force layout recalculation before animating.
33
33
  void getComputedStyle(el).height
34
34
 
35
35
  requestAnimationFrame(() => (el.style.height = height))
@@ -51,20 +51,20 @@ const leave = (element: Element): void => {
51
51
 
52
52
  <style scoped>
53
53
  * {
54
- will-change: height;
55
- transform: translateZ(0);
56
54
  backface-visibility: hidden;
57
55
  perspective: 1000px;
56
+ transform: translateZ(0);
57
+ will-change: height;
58
58
  }
59
59
  </style>
60
60
 
61
61
  <style lang="scss">
62
62
  .expand-enter-active,
63
63
  .expand-leave-active {
64
- transition-property: opacity, height;
64
+ overflow: hidden;
65
65
  transition-duration: 0.2s;
66
+ transition-property: opacity, height;
66
67
  transition-timing-function: ease-out;
67
- overflow: hidden;
68
68
  }
69
69
 
70
70
  .expand-enter-from,
@@ -5,7 +5,7 @@
5
5
  </template>
6
6
 
7
7
  <script setup lang="ts">
8
- import { computed, ref, onMounted, onBeforeUnmount, useId, nextTick } from 'vue'
8
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId } from 'vue'
9
9
 
10
10
  interface Props {
11
11
  type?: 'width' | 'height'
@@ -0,0 +1,190 @@
1
+ <template>
2
+ <div class="cpTrip">
3
+ <div class="cpTrip__content">
4
+ <div class="cpTrip__routePoint">
5
+ <time class="cpTrip__time" :datetime="trip.departureDate.raw">{{ trip.departureTime }}</time>
6
+ <span class="cpTrip__iata">{{ trip.origin.iataCode }}</span>
7
+ </div>
8
+ <cp-trip-timeline
9
+ :airlines="uniqueAirlines"
10
+ :hide-airlines="hideAirlines"
11
+ class="cpTrip__timeline"
12
+ :flight-duration="trip.flightDuration.formatted"
13
+ :has-self-transfer="trip.hasSelfTransfer"
14
+ :labels="trip.labels"
15
+ :stops="trip.stops"
16
+ />
17
+ <div class="cpTrip__routePoint">
18
+ <time class="cpTrip__time" :datetime="trip.arrivalDate.raw">{{ trip.arrivalTime }}</time>
19
+ <span class="cpTrip__iata">
20
+ {{ trip.destination.iataCode }}
21
+ <span v-if="trip.dayCountAfterDeparture" class="cpTrip__plus">+{{ trip.dayCountAfterDeparture }}</span>
22
+ </span>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { computed } from 'vue'
30
+
31
+ import CpTripTimeline from '@/components/CpTripTimeline.vue'
32
+
33
+ type Trip = {
34
+ airlines: {
35
+ iataCode: string
36
+ name: string
37
+ }[];
38
+ arrivalDate: {
39
+ formatted: string
40
+ raw: string
41
+ }
42
+ arrivalTime: string
43
+ dayCountAfterDeparture?: number
44
+ departureDate: {
45
+ formatted: string
46
+ raw: string
47
+ }
48
+ departureTime: string
49
+ destination: {
50
+ iataCode: string
51
+ name: string
52
+ }
53
+ flightDuration: {
54
+ formatted: string
55
+ raw: string
56
+ }
57
+ hasSelfTransfer?: boolean
58
+ labels?: {
59
+ layover?: string
60
+ nonStop?: string
61
+ stopCount?: string
62
+ transfer?: string
63
+ }
64
+ origin: {
65
+ iataCode: string
66
+ name: string
67
+ }
68
+ stops?: {
69
+ duration: string
70
+ iata: string
71
+ name: string
72
+ }[]
73
+ }
74
+
75
+ interface Props {
76
+ hideAirlines?: boolean
77
+ trip: Trip
78
+ }
79
+
80
+ const props = defineProps<Props>()
81
+
82
+ const uniqueAirlines = computed(() => {
83
+ const allAirlinesIataCodes = props.trip.airlines.map((airline) => airline.iataCode)
84
+ const uniqueAirlineIatas: string[] = Array.from(new Set(allAirlinesIataCodes))
85
+
86
+ const allUniqueAirlines = uniqueAirlineIatas.map((uniqueIataCode: string) => {
87
+ return props.trip.airlines.find((airline) => airline.iataCode === uniqueIataCode)
88
+ })
89
+
90
+ return allUniqueAirlines as {
91
+ iataCode: string
92
+ name: string
93
+ }[]
94
+ })
95
+ </script>
96
+
97
+ <style lang="scss">
98
+ .cpTrip {
99
+ &__header {
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: space-between;
103
+ }
104
+
105
+ &__title {
106
+ color: var(--cp-text-secondary);
107
+ font-size: var(--cp-text-size-xs);
108
+ font-weight: 600;
109
+ line-height: var(--cp-line-height-xs);
110
+ padding-block: var(--cp-spacing-sm);
111
+ text-transform: uppercase;
112
+ }
113
+
114
+ &__content {
115
+ display: flex;
116
+ align-items: center;
117
+ gap: var(--cp-spacing-2xl);
118
+ }
119
+
120
+ &__timeline {
121
+ min-width: 0;
122
+ flex: 1;
123
+ }
124
+
125
+ &__routePoint {
126
+ position: relative;
127
+ display: flex;
128
+ flex-direction: column;
129
+ justify-content: center;
130
+
131
+ &:last-child {
132
+ align-items: flex-end;
133
+ }
134
+ }
135
+
136
+ &__plus {
137
+ position: absolute;
138
+ top: fn.px-to-rem(-2);
139
+ right: fn.px-to-rem(-12);
140
+ color: var(--cp-text-error-primary);
141
+ font-size: fn.px-to-rem(10);
142
+ line-height: fn.px-to-rem(14);
143
+ }
144
+
145
+ &__time {
146
+ font-size: var(--cp-text-size-xl);
147
+ font-weight: 600;
148
+ line-height: var(--cp-line-height-xl);
149
+ }
150
+
151
+ &__iata {
152
+ color: var(--cp-text-secondary);
153
+ font-size: var(--cp-text-size-md);
154
+ font-weight: 500;
155
+ line-height: var(--cp-line-height-md);
156
+ }
157
+ }
158
+
159
+ @include mx.media-query-max(370px) {
160
+ .cpTrip {
161
+ &__content {
162
+ gap: var(--cp-spacing-md);
163
+ }
164
+ }
165
+ }
166
+
167
+ @include mx.media-query-max(300px) {
168
+ .cpTrip {
169
+ &__content {
170
+ justify-content: center;
171
+ gap: var(--cp-spacing-sm);
172
+ }
173
+
174
+ &__time {
175
+ font-size: var(--cp-text-size-lg);
176
+ line-height: var(--cp-line-height-sm);
177
+ }
178
+
179
+ &__iata {
180
+ font-size: var(--cp-text-size-sm);
181
+ line-height: var(--cp-line-height-sm);
182
+ }
183
+
184
+ &__plus {
185
+ font-size: fn.px-to-rem(8);
186
+ line-height: fn.px-to-rem(10);
187
+ }
188
+ }
189
+ }
190
+ </style>
@@ -0,0 +1,272 @@
1
+ <template>
2
+ <div class="cpTripTimeline">
3
+ <div class="cpTripTimeline__information">
4
+ <div class="cpTripTimeline__dotWrapper">
5
+ <span class="cpTripTimeline__dot" />
6
+ </div>
7
+ <cp-badge :size="badgeDynamicSize">
8
+ {{ flightDuration }}
9
+ </cp-badge>
10
+ <cp-tooltip v-if="!hideAirlines" allow-outside-container>
11
+ <div class="cpTripTimeline__airlines">
12
+ <div
13
+ v-for="(airline, index) in firstAirlines"
14
+ :key="airline.iataCode"
15
+ class="cpTripTimeline__airlineLogo"
16
+ >
17
+ <span v-if="displayRemainingAirlinesCount(index)" class="cpTripTimeline__remaining">+{{ remainingAirlinesCount }}</span>
18
+ <cp-airline-logo v-else :iata-code="airline.iataCode" :size="logoDynamicSize" />
19
+ </div>
20
+ </div>
21
+ <template #content>
22
+ <ul class="cpTripTimeline__airlinesList">
23
+ <li v-for="airline in airlines" :key="airline.iataCode">{{ airline.name }}</li>
24
+ </ul>
25
+ </template>
26
+ </cp-tooltip>
27
+ <div class="cpTripTimeline__dotWrapper">
28
+ <span class="cpTripTimeline__dot" />
29
+ </div>
30
+ </div>
31
+ <cp-tooltip class="cpTripTimeline__tooltip" :disabled="!hasStops" allow-outside-container>
32
+ <p class="cpTripTimeline__summary">
33
+ <template v-if="hasStops">
34
+ <span class="cpTripTimeline__stop">{{ stopLabel }}</span>
35
+ <span v-if="hasSelfTransfer" class="cpTripTimeline__transfer">
36
+ {{ transferLabel }}
37
+ </span>
38
+ <span>{{ formattedStops }}</span>
39
+ </template>
40
+ <span v-else>{{ nonStopLabel }}</span>
41
+ </p>
42
+ <template #content>
43
+ <ul class="cpTripTimeline__stops">
44
+ <li v-for="stop in stops" :key="stop.iata">
45
+ {{ stop.duration }} {{ layoverLabel }} •
46
+ <template v-if="stop.selfTransferLabel">
47
+ {{ stop.selfTransferLabel }}
48
+ </template>
49
+ <template v-else> {{ stop.name }} ({{ stop.iata }})</template>
50
+
51
+ <hr />
52
+ </li>
53
+ </ul>
54
+ </template>
55
+ </cp-tooltip>
56
+ </div>
57
+ </template>
58
+
59
+ <script setup lang="ts">
60
+ import { computed } from 'vue'
61
+
62
+ import { useDynamicSize } from '@/composables/useDynamicSize'
63
+
64
+ interface Props {
65
+ airlines: {
66
+ iataCode: string;
67
+ name: string;
68
+ }[];
69
+ flightDuration: string;
70
+ hasSelfTransfer?: boolean;
71
+ hideAirlines?: boolean;
72
+ labels?: {
73
+ layover?: string;
74
+ nonStop?: string;
75
+ stopCount?: string;
76
+ transfer?: string;
77
+ };
78
+ stops?: {
79
+ duration: string;
80
+ iata: string;
81
+ name: string;
82
+ selfTransferLabel?: string;
83
+ }[];
84
+ }
85
+
86
+ const props = defineProps<Props>()
87
+
88
+ const stopLabel = computed(() => props.labels?.stopCount)
89
+ const layoverLabel = computed(() => props.labels?.layover)
90
+ const nonStopLabel = computed(() => props.labels?.nonStop)
91
+ const transferLabel = computed(() => props.labels?.transfer)
92
+
93
+ const badgeDynamicSize = useDynamicSize({ default: 'sm', 300: 'xs' })
94
+ const logoDynamicSize = useDynamicSize({ default: 24, 300: 20 })
95
+
96
+ const hasStops = computed(() => !!props.stops?.length)
97
+
98
+ const AIRLINES_LIMIT = 3
99
+ const firstAirlines = computed(() => props.airlines.slice(0, AIRLINES_LIMIT))
100
+ const hasMoreAirlinesThanLimit = computed(() => props.airlines.length > (AIRLINES_LIMIT + 1))
101
+
102
+ const formattedStops = computed(() => {
103
+ if (!hasStops.value) return ''
104
+ return props.stops!.map(({ name }) => name).join(', ')
105
+ })
106
+
107
+ const remainingAirlinesCount = computed(() => props.airlines.length - firstAirlines.value.length)
108
+
109
+ const displayRemainingAirlinesCount = (index: number) => {
110
+ const isLastItem = index === firstAirlines.value.length - 1
111
+ return hasMoreAirlinesThanLimit.value && isLastItem
112
+ }
113
+ </script>
114
+
115
+ <style lang="scss">
116
+ .cpTripTimeline {
117
+ position: relative;
118
+ display: flex;
119
+ flex-direction: column;
120
+ align-items: center;
121
+
122
+ &__information {
123
+ display: flex;
124
+ overflow: hidden;
125
+ width: 100%;
126
+ align-items: center;
127
+ justify-content: center;
128
+ gap: var(--cp-spacing-md);
129
+ padding-block: fn.px-to-rem(3);
130
+
131
+ &::before,
132
+ &::after {
133
+ display: block;
134
+ width: 100%;
135
+ min-width: 0;
136
+ height: fn.px-to-rem(2);
137
+ flex: 1 1 fn.px-to-rem(32);
138
+ border-radius: var(--cp-radius-xs);
139
+ background-color: var(--cp-border-soft);
140
+ content: "";
141
+ transform-origin: right;
142
+ transition: transform 300ms var(--cp-easing-elastic);
143
+ }
144
+ }
145
+
146
+ &__dotWrapper {
147
+ display: flex;
148
+ width: fn.px-to-rem(16);
149
+ height: fn.px-to-rem(16);
150
+ align-items: center;
151
+ justify-content: center;
152
+
153
+ &:first-child {
154
+ order: -1;
155
+ }
156
+
157
+ &:last-child {
158
+ order: 1;
159
+ }
160
+ }
161
+
162
+ &__dot {
163
+ width: fn.px-to-rem(6);
164
+ height: fn.px-to-rem(6);
165
+ border-radius: var(--cp-radius-full);
166
+ background-color: var(--cp-foreground-quaternary);
167
+ }
168
+
169
+ &__airlines {
170
+ position: relative;
171
+ display: flex;
172
+ flex-direction: row;
173
+ align-items: center;
174
+ gap: var(--cp-spacing-xs);
175
+
176
+ > *:not(:first-child) {
177
+ margin-left: calc(var(--cp-spacing-md) * -1);
178
+ }
179
+ }
180
+
181
+ &__remaining {
182
+ display: flex;
183
+ width: fn.px-to-rem(24);
184
+ height: fn.px-to-rem(24);
185
+ align-items: center;
186
+ justify-content: center;
187
+ border-radius: var(--cp-radius-sm);
188
+ background: var(--cp-background-tertiary);
189
+ font-size: var(--cp-text-size-xs);
190
+ font-weight: 600;
191
+ line-height: var(--cp-line-height-xs);
192
+ }
193
+
194
+ &__tooltip {
195
+ display: inline-flex;
196
+ width: 100%;
197
+ justify-content: center;
198
+ padding-block: var(--cp-spacing-sm);
199
+ }
200
+
201
+ &__summary {
202
+ @extend %u-text-ellipsis;
203
+
204
+ color: var(--cp-text-secondary);
205
+ font-size: var(--cp-text-size-xs);
206
+ line-height: var(--cp-line-height-xs);
207
+
208
+ &:has(.cpTripTimeline__stop) {
209
+ border-bottom: 1px dashed var(--cp-border-strong);
210
+ cursor: help;
211
+ }
212
+
213
+ span:not(:first-child)::before {
214
+ color: var(--cp-text-secondary);
215
+ content: "•";
216
+ font-size: var(--cp-text-size-xs);
217
+ letter-spacing: var(--cp-letter-spacing-xs);
218
+ line-height: var(--cp-line-height-xs);
219
+ margin-inline: var(--cp-spacing-xs);
220
+ }
221
+ }
222
+
223
+ &__transfer {
224
+ color: var(--cp-text-error-primary);
225
+ text-transform: capitalize;
226
+ }
227
+
228
+ &__stops {
229
+ max-width: fn.px-to-rem(300);
230
+ }
231
+
232
+ &__stops li hr {
233
+ border-radius: var(--cp-radius-md);
234
+ margin-block: var(--cp-spacing-md);
235
+ }
236
+
237
+ &__stops li:last-child hr {
238
+ display: none;
239
+ }
240
+ }
241
+
242
+ @include mx.media-query-max(370px) {
243
+ .cpTripTimeline {
244
+ &__dotWrapper {
245
+ display: none;
246
+ }
247
+ }
248
+ }
249
+
250
+ @include mx.media-query-max(300px) {
251
+ .cpTripTimeline {
252
+ &__information {
253
+ padding-block: 0;
254
+
255
+ &::before,
256
+ &::after {
257
+ display: none;
258
+ }
259
+ }
260
+
261
+ &__remaining {
262
+ width: fn.px-to-rem(20);
263
+ height: fn.px-to-rem(20);
264
+ font-size: fn.px-to-rem(10);
265
+ }
266
+
267
+ &__tooltip {
268
+ padding-block: var(--cp-spacing-xs);
269
+ }
270
+ }
271
+ }
272
+ </style>