@citizenplane/pimp 18.14.3 → 18.16.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.
- package/dist/components/CpSelectableButton.vue.d.ts.map +1 -1
- package/dist/components/CpTextMorph.vue.d.ts +11 -0
- package/dist/components/CpTextMorph.vue.d.ts.map +1 -0
- package/dist/components/CpTripDetails.vue.d.ts +14 -0
- package/dist/components/CpTripDetails.vue.d.ts.map +1 -0
- package/dist/components/CpTripLegRouteInformation.vue.d.ts +11 -0
- package/dist/components/CpTripLegRouteInformation.vue.d.ts.map +1 -0
- package/dist/components/CpTripLegRouteLayover.vue.d.ts +13 -0
- package/dist/components/CpTripLegRouteLayover.vue.d.ts.map +1 -0
- package/dist/components/CpTripLegRoutePlace.vue.d.ts +22 -0
- package/dist/components/CpTripLegRoutePlace.vue.d.ts.map +1 -0
- package/dist/components/CpTripLegRouteSegment.vue.d.ts +18 -0
- package/dist/components/CpTripLegRouteSegment.vue.d.ts.map +1 -0
- package/dist/components/index.d.ts +8 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/constants/tripDetails/Airline.d.ts +5 -0
- package/dist/constants/tripDetails/Airline.d.ts.map +1 -0
- package/dist/constants/tripDetails/Airport.d.ts +7 -0
- package/dist/constants/tripDetails/Airport.d.ts.map +1 -0
- package/dist/constants/tripDetails/CpTripDetailsConfig.d.ts +16 -0
- package/dist/constants/tripDetails/CpTripDetailsConfig.d.ts.map +1 -0
- package/dist/constants/tripDetails/Flight.d.ts +5 -0
- package/dist/constants/tripDetails/Flight.d.ts.map +1 -0
- package/dist/constants/tripDetails/FlightSegment.d.ts +15 -0
- package/dist/constants/tripDetails/FlightSegment.d.ts.map +1 -0
- package/dist/constants/tripDetails/FlightStopTypes.d.ts +7 -0
- package/dist/constants/tripDetails/FlightStopTypes.d.ts.map +1 -0
- package/dist/constants/tripDetails/Terminal.d.ts +5 -0
- package/dist/constants/tripDetails/Terminal.d.ts.map +1 -0
- package/dist/constants/tripDetails/index.d.ts +9 -0
- package/dist/constants/tripDetails/index.d.ts.map +1 -0
- package/dist/helpers/tripDetails.d.ts +23 -0
- package/dist/helpers/tripDetails.d.ts.map +1 -0
- package/dist/pimp.es.js +5656 -4673
- package/dist/pimp.umd.js +66 -33
- package/dist/style.css +1 -1
- package/package.json +2 -1
- package/src/assets/images/dot.svg +3 -0
- package/src/components/CpSelectableButton.vue +4 -0
- package/src/components/CpTextMorph.vue +17 -0
- package/src/components/CpTripDetails.vue +64 -0
- package/src/components/CpTripLegRouteInformation.vue +224 -0
- package/src/components/CpTripLegRouteLayover.vue +63 -0
- package/src/components/CpTripLegRoutePlace.vue +196 -0
- package/src/components/CpTripLegRouteSegment.vue +99 -0
- package/src/components/index.ts +26 -0
- package/src/constants/tripDetails/Airline.ts +4 -0
- package/src/constants/tripDetails/Airport.ts +6 -0
- package/src/constants/tripDetails/CpTripDetailsConfig.ts +29 -0
- package/src/constants/tripDetails/Flight.ts +5 -0
- package/src/constants/tripDetails/FlightSegment.ts +15 -0
- package/src/constants/tripDetails/FlightStopTypes.ts +6 -0
- package/src/constants/tripDetails/Terminal.ts +4 -0
- package/src/constants/tripDetails/index.ts +9 -0
- package/src/helpers/tripDetails.ts +61 -0
- package/src/stories/CpTextMorph.stories.ts +236 -0
- package/src/stories/CpTrip.stories.ts +147 -0
- package/src/stories/CpTripDetails.stories.ts +134 -0
package/src/components/index.ts
CHANGED
|
@@ -63,6 +63,7 @@ import CpTabs from './CpTabs.vue'
|
|
|
63
63
|
import CpTelInput from './CpTelInput.vue'
|
|
64
64
|
import CpText from './CpText.vue'
|
|
65
65
|
import CpTextarea from './CpTextarea.vue'
|
|
66
|
+
import CpTextMorph from './CpTextMorph.vue'
|
|
66
67
|
import CpToast from './CpToast.vue'
|
|
67
68
|
import CpTooltip from './CpTooltip.vue'
|
|
68
69
|
import CpTransitionCounter from './CpTransitionCounter.vue'
|
|
@@ -73,6 +74,11 @@ import CpTransitionSize from './CpTransitionSize.vue'
|
|
|
73
74
|
import CpTransitionSlide from './CpTransitionSlide.vue'
|
|
74
75
|
import CpTransitionTabContent from './CpTransitionTabContent.vue'
|
|
75
76
|
import CpTrip from './CpTrip.vue'
|
|
77
|
+
import CpTripDetails from './CpTripDetails.vue'
|
|
78
|
+
import CpTripLegRouteInformation from './CpTripLegRouteInformation.vue'
|
|
79
|
+
import CpTripLegRouteLayover from './CpTripLegRouteLayover.vue'
|
|
80
|
+
import CpTripLegRoutePlace from './CpTripLegRoutePlace.vue'
|
|
81
|
+
import CpTripLegRouteSegment from './CpTripLegRouteSegment.vue'
|
|
76
82
|
import IconAirline from './icons/IconAirline.vue'
|
|
77
83
|
import IconCheckList from './icons/IconCheckList.vue'
|
|
78
84
|
import IconCollapse from './icons/IconCollapse.vue'
|
|
@@ -125,6 +131,7 @@ const Components = {
|
|
|
125
131
|
CpIcon,
|
|
126
132
|
CpTelInput,
|
|
127
133
|
CpTooltip,
|
|
134
|
+
CpTextMorph,
|
|
128
135
|
CpAirlineLogo,
|
|
129
136
|
IconAirline,
|
|
130
137
|
IconOta,
|
|
@@ -143,6 +150,11 @@ const Components = {
|
|
|
143
150
|
CpTransitionSlide,
|
|
144
151
|
CpTransitionTabContent,
|
|
145
152
|
CpTrip,
|
|
153
|
+
CpTripDetails,
|
|
154
|
+
CpTripLegRouteInformation,
|
|
155
|
+
CpTripLegRouteLayover,
|
|
156
|
+
CpTripLegRoutePlace,
|
|
157
|
+
CpTripLegRouteSegment,
|
|
146
158
|
CpSeatMap,
|
|
147
159
|
CpSeatMapEmergencyExitDialog,
|
|
148
160
|
CpSeatMapGrid,
|
|
@@ -184,6 +196,14 @@ export type {
|
|
|
184
196
|
SeatRowFacilityFlags,
|
|
185
197
|
} from '@/constants/seatMap'
|
|
186
198
|
export type { CpSeatMapSeatSelection as CpSeatMapSelection } from '@/constants/seatMap/CpSeatMapSeatSelection'
|
|
199
|
+
export type {
|
|
200
|
+
CpTripDetailsConfig,
|
|
201
|
+
Flight,
|
|
202
|
+
FlightAirline,
|
|
203
|
+
FlightAirport,
|
|
204
|
+
FlightSegment,
|
|
205
|
+
FlightTerminal,
|
|
206
|
+
} from '@/constants/tripDetails'
|
|
187
207
|
|
|
188
208
|
export {
|
|
189
209
|
CpSeatMapTransitionDirections,
|
|
@@ -194,6 +214,7 @@ export {
|
|
|
194
214
|
SeatRowFacilityPosition,
|
|
195
215
|
SeatStatuses,
|
|
196
216
|
} from '@/constants/seatMap'
|
|
217
|
+
export { DEFAULT_CP_TRIP_DETAILS_CONFIG, FlightStopTypes } from '@/constants/tripDetails'
|
|
197
218
|
// Consumers key `selections` with this, so it has to be the very same helper.
|
|
198
219
|
export { displaySeat } from '@/helpers/seatMap'
|
|
199
220
|
export {
|
|
@@ -256,6 +277,11 @@ export {
|
|
|
256
277
|
CpTransitionSlide,
|
|
257
278
|
CpTransitionTabContent,
|
|
258
279
|
CpTrip,
|
|
280
|
+
CpTripDetails,
|
|
281
|
+
CpTripLegRouteInformation,
|
|
282
|
+
CpTripLegRouteLayover,
|
|
283
|
+
CpTripLegRoutePlace,
|
|
284
|
+
CpTripLegRouteSegment,
|
|
259
285
|
IconAirline,
|
|
260
286
|
IconCheckList,
|
|
261
287
|
IconCollapse,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface CpTripDetailsConfig {
|
|
2
|
+
routeInformation: {
|
|
3
|
+
airline: string
|
|
4
|
+
connectionInfo: string
|
|
5
|
+
flightNo: string
|
|
6
|
+
technicalStop: string
|
|
7
|
+
}
|
|
8
|
+
routeLayover: {
|
|
9
|
+
layover: string
|
|
10
|
+
}
|
|
11
|
+
routePlace: {
|
|
12
|
+
terminalNotSpecified: string
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_CP_TRIP_DETAILS_CONFIG: CpTripDetailsConfig = {
|
|
17
|
+
routeLayover: {
|
|
18
|
+
layover: 'layover',
|
|
19
|
+
},
|
|
20
|
+
routePlace: {
|
|
21
|
+
terminalNotSpecified: 'Terminal not specified',
|
|
22
|
+
},
|
|
23
|
+
routeInformation: {
|
|
24
|
+
connectionInfo: 'Connection info',
|
|
25
|
+
airline: 'airline',
|
|
26
|
+
flightNo: 'Flight No',
|
|
27
|
+
technicalStop: 'Technical stop',
|
|
28
|
+
},
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FlightAirline } from './Airline'
|
|
2
|
+
import type { FlightAirport } from './Airport'
|
|
3
|
+
import type { FlightTerminal } from './Terminal'
|
|
4
|
+
|
|
5
|
+
export interface FlightSegment {
|
|
6
|
+
airline: FlightAirline
|
|
7
|
+
arrival_date: string
|
|
8
|
+
departure_date: string
|
|
9
|
+
destination: FlightAirport
|
|
10
|
+
destination_terminal?: FlightTerminal
|
|
11
|
+
flight_number: string
|
|
12
|
+
origin: FlightAirport
|
|
13
|
+
origin_terminal?: FlightTerminal
|
|
14
|
+
via_airport?: Pick<FlightAirport, 'city_name' | 'iata_code'>
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type { FlightAirline } from './Airline'
|
|
2
|
+
export type { FlightAirport } from './Airport'
|
|
3
|
+
export type { CpTripDetailsConfig } from './CpTripDetailsConfig'
|
|
4
|
+
export type { Flight } from './Flight'
|
|
5
|
+
export type { FlightSegment } from './FlightSegment'
|
|
6
|
+
export type { FlightTerminal } from './Terminal'
|
|
7
|
+
|
|
8
|
+
export { DEFAULT_CP_TRIP_DETAILS_CONFIG } from './CpTripDetailsConfig'
|
|
9
|
+
export { FlightStopTypes } from './FlightStopTypes'
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { DateTime, Duration } from 'luxon'
|
|
2
|
+
|
|
3
|
+
const DURATION_PLACEHOLDER = '-- h -- min'
|
|
4
|
+
|
|
5
|
+
// Luxon ships no types in Pimp (see src/types/luxon.d.ts) — this is the slice of
|
|
6
|
+
// DateTime/Duration's shape CpTripDetails actually relies on.
|
|
7
|
+
export interface TripDuration {
|
|
8
|
+
shiftTo(...units: string[]): TripDuration
|
|
9
|
+
toHuman(opts: { maximumFractionDigits?: number; unitDisplay?: string }): string
|
|
10
|
+
toISO(): string | null
|
|
11
|
+
toObject(): Record<string, number>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TripDateTime {
|
|
15
|
+
diff(other: TripDateTime, units: string[]): TripDuration
|
|
16
|
+
toFormat(format: string): string
|
|
17
|
+
toISO(): string | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const fromGMTToLocal = (date: string, zone: string, locale = 'en'): TripDateTime => {
|
|
21
|
+
return DateTime.fromISO(date, { zone: 'utc' }).setZone(zone).setLocale(locale)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const getDuration = (end: TripDateTime, start: TripDateTime): TripDuration => {
|
|
25
|
+
return end.diff(start, ['hours', 'minutes'])
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const getFormattedDuration = (duration?: TripDuration | null, locale = 'en'): string => {
|
|
29
|
+
if (!duration) return DURATION_PLACEHOLDER
|
|
30
|
+
|
|
31
|
+
// Concatenates values to make toHuman more readable (ex: 26h becomes 1d, 2hr)
|
|
32
|
+
const shiftedDuration = duration.shiftTo('hours', 'minutes').toObject()
|
|
33
|
+
const entries = Object.entries(shiftedDuration).filter(([, amount]) => amount)
|
|
34
|
+
const formattedDuration = Duration.fromObject(Object.fromEntries(entries), { locale })
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
formattedDuration.toHuman({
|
|
38
|
+
unitDisplay: 'short',
|
|
39
|
+
maximumFractionDigits: 0,
|
|
40
|
+
}) || DURATION_PLACEHOLDER
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const getDayCountBetweenDates = ({
|
|
45
|
+
departureDate,
|
|
46
|
+
arrivalDate,
|
|
47
|
+
isArrival = false,
|
|
48
|
+
}: {
|
|
49
|
+
arrivalDate: TripDateTime
|
|
50
|
+
departureDate: TripDateTime
|
|
51
|
+
isArrival?: boolean
|
|
52
|
+
}): number => {
|
|
53
|
+
const departureDay = departureDate.toFormat('yyyy-LL-dd')
|
|
54
|
+
const arrivalDay = arrivalDate.toFormat('yyyy-LL-dd')
|
|
55
|
+
|
|
56
|
+
if (isArrival) {
|
|
57
|
+
return DateTime.fromISO(departureDay).diff(DateTime.fromISO(arrivalDay), 'days').days
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return DateTime.fromISO(arrivalDay).diff(DateTime.fromISO(departureDay), 'days').days
|
|
61
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
import CpButton from '@/components/CpButton.vue'
|
|
5
|
+
import CpTextMorph from '@/components/CpTextMorph.vue'
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: 'Transitions/CpTextMorph',
|
|
9
|
+
component: CpTextMorph,
|
|
10
|
+
argTypes: {
|
|
11
|
+
text: {
|
|
12
|
+
control: 'text',
|
|
13
|
+
description: 'Text to display. Changing it morphs from the previous value.',
|
|
14
|
+
},
|
|
15
|
+
duration: {
|
|
16
|
+
control: 'number',
|
|
17
|
+
description: 'Duration of the morph in milliseconds.',
|
|
18
|
+
},
|
|
19
|
+
disabled: {
|
|
20
|
+
control: 'boolean',
|
|
21
|
+
description: 'Whether the morph is disabled.',
|
|
22
|
+
},
|
|
23
|
+
as: {
|
|
24
|
+
control: 'text',
|
|
25
|
+
description: 'HTML tag to render the component as.',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
args: {
|
|
29
|
+
duration: 400,
|
|
30
|
+
disabled: false,
|
|
31
|
+
as: 'span',
|
|
32
|
+
},
|
|
33
|
+
} satisfies Meta<typeof CpTextMorph>
|
|
34
|
+
|
|
35
|
+
export default meta
|
|
36
|
+
type Story = StoryObj<typeof meta>
|
|
37
|
+
|
|
38
|
+
const wrapperStyle = 'display: flex; flex-direction: column; align-items: flex-start; gap: 24px;'
|
|
39
|
+
|
|
40
|
+
const singleLineStyle = 'font-size: 2rem; font-weight: 600; line-height: 1.2;'
|
|
41
|
+
|
|
42
|
+
const multiLineStyle = 'font-size: 1.25rem; line-height: 1.6;'
|
|
43
|
+
|
|
44
|
+
const singleLineTexts = ['Search for flights', 'Choose your seat']
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Single line text. Use the button to swap the value and watch the
|
|
48
|
+
* characters morph from one label to the other.
|
|
49
|
+
*/
|
|
50
|
+
export const Default: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
text: singleLineTexts[0],
|
|
53
|
+
},
|
|
54
|
+
render: (args) => ({
|
|
55
|
+
components: { CpTextMorph, CpButton },
|
|
56
|
+
setup() {
|
|
57
|
+
const isMorphed = ref(false)
|
|
58
|
+
const text = computed(() => (isMorphed.value ? singleLineTexts[1] : args.text))
|
|
59
|
+
|
|
60
|
+
const toggle = () => {
|
|
61
|
+
isMorphed.value = !isMorphed.value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const buttonLabel = computed(() => (isMorphed.value ? 'Unmorph text' : 'Morph text'))
|
|
65
|
+
|
|
66
|
+
return { args, text, toggle, buttonLabel }
|
|
67
|
+
},
|
|
68
|
+
template: `
|
|
69
|
+
<div style="${wrapperStyle}">
|
|
70
|
+
<div style="${singleLineStyle}">
|
|
71
|
+
<CpTextMorph v-bind="args" :text="text" />
|
|
72
|
+
</div>
|
|
73
|
+
<CpButton appearance="primary" color="accent" @click="toggle">
|
|
74
|
+
<CpTextMorph v-bind="args" :text="buttonLabel" />
|
|
75
|
+
</CpButton>
|
|
76
|
+
</div>
|
|
77
|
+
`,
|
|
78
|
+
}),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Lines are explicit: torph keeps its content on a single line unless the value contains newlines. */
|
|
82
|
+
const multiLineTexts = [
|
|
83
|
+
[
|
|
84
|
+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit,',
|
|
85
|
+
'sed do eiusmod tempor incididunt ut labore et dolore',
|
|
86
|
+
'magna aliqua veniam quis nostrud.',
|
|
87
|
+
].join('\n'),
|
|
88
|
+
[
|
|
89
|
+
'Lorem ipsum dolor sit amet, quis nostrud exercitation',
|
|
90
|
+
'ullamco laboris nisi ut aliquip ex ea commodo dolore',
|
|
91
|
+
'magna aliqua consectetur adipiscing elit.',
|
|
92
|
+
].join('\n'),
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Multi line paragraph. Both values share most of their words, so the
|
|
97
|
+
* morph mostly moves existing words to their new position across lines
|
|
98
|
+
* instead of fading everything in and out.
|
|
99
|
+
*/
|
|
100
|
+
export const MultiLine: Story = {
|
|
101
|
+
args: {
|
|
102
|
+
text: multiLineTexts[0],
|
|
103
|
+
},
|
|
104
|
+
render: (args) => ({
|
|
105
|
+
components: { CpTextMorph, CpButton },
|
|
106
|
+
setup() {
|
|
107
|
+
const isMorphed = ref(false)
|
|
108
|
+
const text = computed(() => (isMorphed.value ? multiLineTexts[1] : args.text))
|
|
109
|
+
|
|
110
|
+
const toggle = () => {
|
|
111
|
+
isMorphed.value = !isMorphed.value
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { args, text, toggle }
|
|
115
|
+
},
|
|
116
|
+
template: `
|
|
117
|
+
<div style="${wrapperStyle}">
|
|
118
|
+
<div style="${multiLineStyle}">
|
|
119
|
+
<CpTextMorph v-bind="args" :text="text" />
|
|
120
|
+
</div>
|
|
121
|
+
<CpButton appearance="primary" color="accent" @click="toggle">
|
|
122
|
+
Morph text
|
|
123
|
+
</CpButton>
|
|
124
|
+
</div>
|
|
125
|
+
`,
|
|
126
|
+
}),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const bookingSteps = [
|
|
130
|
+
{
|
|
131
|
+
title: 'Select your flight',
|
|
132
|
+
fields: ['Paris (CDG) → London (LHR)', 'Wednesday 19 August, 09:45', 'Economy, 1 passenger'],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
title: 'Select your seat',
|
|
136
|
+
fields: ['Seat 14A, window', 'Extra legroom', 'Front of the cabin'],
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
title: 'Confirm your booking',
|
|
140
|
+
fields: ['Paris (CDG) → London (LHR)', 'Seat 14A, window', 'Total: 189,00 €'],
|
|
141
|
+
},
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
const dialogFooterStyle = 'display: flex; justify-content: space-between; gap: 8px; width: 100%;'
|
|
145
|
+
|
|
146
|
+
const dialogFieldsStyle = 'display: flex; flex-direction: column; gap: 8px; margin: 0; padding: 0; list-style: none;'
|
|
147
|
+
/**
|
|
148
|
+
* Use case — a multi step dialog whose title morphs on every step
|
|
149
|
+
* instead of being swapped instantly. The step titles share words on
|
|
150
|
+
* purpose ("Select your …"), so only the changing part animates while
|
|
151
|
+
* the rest stays in place.
|
|
152
|
+
*
|
|
153
|
+
* The title is wrapped in an `h2` inside the `title` slot to keep the
|
|
154
|
+
* dialog heading semantics. Around it, several transitions run together:
|
|
155
|
+
* `CpTransitionCounter` rolls the step number, `CpTransitionSize`
|
|
156
|
+
* animates the width of the whole "Step 1 of 3" subtitle and the body
|
|
157
|
+
* height, and the footer keeps a single action button whose label morphs
|
|
158
|
+
* from "Next" to "Confirm" while its width follows.
|
|
159
|
+
*/
|
|
160
|
+
export const MultiStepDialog: Story = {
|
|
161
|
+
args: {
|
|
162
|
+
text: bookingSteps[0].title,
|
|
163
|
+
},
|
|
164
|
+
render: (args) => ({
|
|
165
|
+
components: { CpTextMorph },
|
|
166
|
+
setup() {
|
|
167
|
+
const isOpen = ref(false)
|
|
168
|
+
const stepIndex = ref(0)
|
|
169
|
+
|
|
170
|
+
const step = computed(() => bookingSteps[stepIndex.value])
|
|
171
|
+
const stepLabel = computed(() => `Step ${stepIndex.value + 1} of ${bookingSteps.length}`)
|
|
172
|
+
const isLastStep = computed(() => stepIndex.value === bookingSteps.length - 1)
|
|
173
|
+
const nextLabel = computed(() => (isLastStep.value ? 'Confirm' : 'Next'))
|
|
174
|
+
|
|
175
|
+
const open = () => {
|
|
176
|
+
stepIndex.value = 0
|
|
177
|
+
isOpen.value = true
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const close = () => {
|
|
181
|
+
isOpen.value = false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const goToNextStep = () => {
|
|
185
|
+
if (isLastStep.value) {
|
|
186
|
+
close()
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
stepIndex.value++
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
args,
|
|
194
|
+
isOpen,
|
|
195
|
+
stepIndex,
|
|
196
|
+
step,
|
|
197
|
+
stepLabel,
|
|
198
|
+
nextLabel,
|
|
199
|
+
open,
|
|
200
|
+
close,
|
|
201
|
+
goToNextStep,
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
template: `
|
|
205
|
+
<CpButton appearance="primary" color="accent" @click="open">Book a flight</CpButton>
|
|
206
|
+
<CpTransitionDialog>
|
|
207
|
+
<CpDialog v-if="isOpen" :max-width="480" @close="close">
|
|
208
|
+
<template #title>
|
|
209
|
+
<h2><CpTextMorph v-bind="args" :text="step.title" /></h2>
|
|
210
|
+
</template>
|
|
211
|
+
<template #subtitle>
|
|
212
|
+
<CpTextMorph v-bind="args" :text="stepLabel" />
|
|
213
|
+
</template>
|
|
214
|
+
<ul style="${dialogFieldsStyle}">
|
|
215
|
+
<li v-for="field in step.fields" :key="field">{{ field }}</li>
|
|
216
|
+
</ul>
|
|
217
|
+
<template #footer>
|
|
218
|
+
<div style="${dialogFooterStyle}">
|
|
219
|
+
<CpButton
|
|
220
|
+
appearance="secondary"
|
|
221
|
+
color="neutral"
|
|
222
|
+
:disabled="stepIndex === 0"
|
|
223
|
+
@click="stepIndex--"
|
|
224
|
+
>
|
|
225
|
+
Back
|
|
226
|
+
</CpButton>
|
|
227
|
+
<CpButton appearance="primary" color="accent" @click="goToNextStep">
|
|
228
|
+
<CpTextMorph v-bind="args" :text="nextLabel" />
|
|
229
|
+
</CpButton>
|
|
230
|
+
</div>
|
|
231
|
+
</template>
|
|
232
|
+
</CpDialog>
|
|
233
|
+
</CpTransitionDialog>
|
|
234
|
+
`,
|
|
235
|
+
}),
|
|
236
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Args, Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
2
|
|
|
3
|
+
import type { Flight } from '@/constants/tripDetails/Flight'
|
|
4
|
+
|
|
5
|
+
import CpAccordion from '@/components/CpAccordion.vue'
|
|
3
6
|
import CpTrip from '@/components/CpTrip.vue'
|
|
7
|
+
import CpTripDetails from '@/components/CpTripDetails.vue'
|
|
4
8
|
|
|
5
9
|
const directTrip = {
|
|
6
10
|
arrivalDate: {
|
|
@@ -221,6 +225,104 @@ const fourStopTrip = {
|
|
|
221
225
|
],
|
|
222
226
|
}
|
|
223
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Summary and segments below describe the same one-stop CDG → MUC → JFK
|
|
230
|
+
* itinerary: 09:00 Paris → 10:40 Munich, 2h 10m layover, 12:50 Munich →
|
|
231
|
+
* 16:15 New York, for 13h 15m of total travel time on 10 Jun 2026.
|
|
232
|
+
*/
|
|
233
|
+
const fullTrip = {
|
|
234
|
+
arrivalDate: {
|
|
235
|
+
formatted: 'Wed, 10 Jun 2026',
|
|
236
|
+
raw: '2026-06-10',
|
|
237
|
+
},
|
|
238
|
+
arrivalTime: '16:15',
|
|
239
|
+
dayCountAfterDeparture: 0,
|
|
240
|
+
departureDate: {
|
|
241
|
+
formatted: 'Wed, 10 Jun 2026',
|
|
242
|
+
raw: '2026-06-10',
|
|
243
|
+
},
|
|
244
|
+
departureTime: '09:00',
|
|
245
|
+
destination: {
|
|
246
|
+
iataCode: 'JFK',
|
|
247
|
+
name: 'John F. Kennedy International Airport',
|
|
248
|
+
},
|
|
249
|
+
flightDuration: {
|
|
250
|
+
formatted: '13h 15m',
|
|
251
|
+
raw: 'PT13H15M',
|
|
252
|
+
},
|
|
253
|
+
labels: {
|
|
254
|
+
layover: 'layover',
|
|
255
|
+
stopCount: '1 stop',
|
|
256
|
+
},
|
|
257
|
+
airlines: [
|
|
258
|
+
{
|
|
259
|
+
iataCode: 'AF',
|
|
260
|
+
name: 'Air France',
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
iataCode: 'LH',
|
|
264
|
+
name: 'Lufthansa',
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
origin: {
|
|
268
|
+
iataCode: 'CDG',
|
|
269
|
+
name: 'Paris Charles de Gaulle Airport',
|
|
270
|
+
},
|
|
271
|
+
stops: [
|
|
272
|
+
{
|
|
273
|
+
duration: '2h 10m',
|
|
274
|
+
iata: 'MUC',
|
|
275
|
+
name: 'Munich',
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Segment dates are GMT — `CpTripDetails` converts them to each airport's timezone. */
|
|
281
|
+
const fullTripFlight: Flight = {
|
|
282
|
+
segments: [
|
|
283
|
+
{
|
|
284
|
+
airline: { iata_code: 'AF', name: 'Air France' },
|
|
285
|
+
arrival_date: '2026-06-10T08:40:00',
|
|
286
|
+
departure_date: '2026-06-10T07:00:00',
|
|
287
|
+
destination: {
|
|
288
|
+
city_name: 'Munich',
|
|
289
|
+
iata_code: 'MUC',
|
|
290
|
+
name: 'Franz Josef Strauss Airport',
|
|
291
|
+
timezone: 'Europe/Berlin',
|
|
292
|
+
},
|
|
293
|
+
destination_terminal: { label: 'Terminal 2', terminal_code: '2' },
|
|
294
|
+
flight_number: 'AF1122',
|
|
295
|
+
origin: {
|
|
296
|
+
city_name: 'Paris',
|
|
297
|
+
iata_code: 'CDG',
|
|
298
|
+
name: 'Paris Charles de Gaulle Airport',
|
|
299
|
+
timezone: 'Europe/Paris',
|
|
300
|
+
},
|
|
301
|
+
origin_terminal: { label: 'Terminal 2E', terminal_code: '2E' },
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
airline: { iata_code: 'LH', name: 'Lufthansa' },
|
|
305
|
+
arrival_date: '2026-06-10T20:15:00',
|
|
306
|
+
departure_date: '2026-06-10T10:50:00',
|
|
307
|
+
destination: {
|
|
308
|
+
city_name: 'New York',
|
|
309
|
+
iata_code: 'JFK',
|
|
310
|
+
name: 'John F. Kennedy International Airport',
|
|
311
|
+
timezone: 'America/New_York',
|
|
312
|
+
},
|
|
313
|
+
destination_terminal: { label: 'Terminal 1', terminal_code: '1' },
|
|
314
|
+
flight_number: 'LH410',
|
|
315
|
+
origin: {
|
|
316
|
+
city_name: 'Munich',
|
|
317
|
+
iata_code: 'MUC',
|
|
318
|
+
name: 'Franz Josef Strauss Airport',
|
|
319
|
+
timezone: 'Europe/Berlin',
|
|
320
|
+
},
|
|
321
|
+
origin_terminal: { label: 'Terminal 2', terminal_code: '2' },
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
}
|
|
325
|
+
|
|
224
326
|
const meta = {
|
|
225
327
|
title: 'Business/CpTrip',
|
|
226
328
|
component: CpTrip,
|
|
@@ -321,3 +423,48 @@ export const HideAirlines: Story = {
|
|
|
321
423
|
},
|
|
322
424
|
render: defaultRender,
|
|
323
425
|
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* A complete itinerary: `CpTrip` acts as the always-visible summary in the
|
|
429
|
+
* accordion header, and `CpTripDetails` renders the leg-by-leg breakdown in
|
|
430
|
+
* the expandable content. The rounded corners come from the wrapper, since
|
|
431
|
+
* `CpAccordion` is square by design so it can be stacked in a group.
|
|
432
|
+
*/
|
|
433
|
+
export const FullTrip: Story = {
|
|
434
|
+
args: {
|
|
435
|
+
trip: fullTrip,
|
|
436
|
+
},
|
|
437
|
+
render: (args: Args) => ({
|
|
438
|
+
components: { CpAccordion, CpTrip, CpTripDetails },
|
|
439
|
+
setup() {
|
|
440
|
+
const wrapperStyle = [
|
|
441
|
+
'box-sizing: border-box',
|
|
442
|
+
'width: 640px',
|
|
443
|
+
'max-width: 100%',
|
|
444
|
+
'overflow: hidden',
|
|
445
|
+
'border: 1px solid var(--cp-border-soft)',
|
|
446
|
+
'border-radius: var(--cp-radius-xl)',
|
|
447
|
+
'box-shadow: var(--cp-shadows-xs)',
|
|
448
|
+
].join('; ')
|
|
449
|
+
|
|
450
|
+
const summaryStyle = 'padding-top: var(--cp-spacing-md);'
|
|
451
|
+
const detailsStyle = 'border-top: 1px solid var(--cp-border-soft);'
|
|
452
|
+
|
|
453
|
+
return { args, detailsStyle, fullTripFlight, summaryStyle, wrapperStyle }
|
|
454
|
+
},
|
|
455
|
+
template: `
|
|
456
|
+
<div :style="wrapperStyle">
|
|
457
|
+
<CpAccordion :default-open-state="true" icon-position="trailing" title="Outbound · Wed, 10 Jun 2026">
|
|
458
|
+
<template #leading-slot>
|
|
459
|
+
<div :style="summaryStyle">
|
|
460
|
+
<CpTrip v-bind="args" />
|
|
461
|
+
</div>
|
|
462
|
+
</template>
|
|
463
|
+
<div :style="detailsStyle">
|
|
464
|
+
<CpTripDetails :flight="fullTripFlight" />
|
|
465
|
+
</div>
|
|
466
|
+
</CpAccordion>
|
|
467
|
+
</div>
|
|
468
|
+
`,
|
|
469
|
+
}),
|
|
470
|
+
}
|