@citizenplane/pimp 18.14.4 → 18.16.1
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/CpMultiselect.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 +5496 -4508
- package/dist/pimp.umd.js +67 -34
- package/dist/style.css +1 -1
- package/package.json +2 -1
- package/src/assets/images/dot.svg +3 -0
- package/src/components/CpMultiselect.vue +23 -1
- 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/CpMultiselect.stories.ts +30 -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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cpTripLegRouteSegment">
|
|
3
|
+
<cp-trip-leg-route-layover
|
|
4
|
+
v-if="isLayoverSectionVisible"
|
|
5
|
+
:config="config"
|
|
6
|
+
:duration="stopDuration"
|
|
7
|
+
:locale="locale"
|
|
8
|
+
/>
|
|
9
|
+
<cp-trip-leg-route-place
|
|
10
|
+
:city="origin.name"
|
|
11
|
+
class="cpTripLegRouteSegment__place"
|
|
12
|
+
:config="config"
|
|
13
|
+
:day-count-after-arrival="dayCountAfterArrival"
|
|
14
|
+
:iata="origin.iata_code"
|
|
15
|
+
:terminal="segment.origin_terminal"
|
|
16
|
+
:time="departureDate"
|
|
17
|
+
:type="firstAirportType"
|
|
18
|
+
/>
|
|
19
|
+
<cp-trip-leg-route-information :config="config" :flight-duration="segmentDuration" :segment="segment" />
|
|
20
|
+
<cp-trip-leg-route-place
|
|
21
|
+
:city="destination.name"
|
|
22
|
+
class="cpTripLegRouteSegment__place"
|
|
23
|
+
:config="config"
|
|
24
|
+
:day-count-after-departure="dayCountAfterDeparture"
|
|
25
|
+
:iata="destination.iata_code"
|
|
26
|
+
:terminal="segment.destination_terminal"
|
|
27
|
+
:time="arrivalDate"
|
|
28
|
+
:type="secondAirportType"
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
import { computed } from 'vue'
|
|
35
|
+
|
|
36
|
+
import type { CpTripDetailsConfig } from '@/constants/tripDetails/CpTripDetailsConfig'
|
|
37
|
+
import type { FlightSegment } from '@/constants/tripDetails/FlightSegment'
|
|
38
|
+
import type { TripDateTime, TripDuration } from '@/helpers/tripDetails'
|
|
39
|
+
|
|
40
|
+
import { FlightStopTypes } from '@/constants/tripDetails/FlightStopTypes'
|
|
41
|
+
import { fromGMTToLocal, getDayCountBetweenDates, getDuration, getFormattedDuration } from '@/helpers/tripDetails'
|
|
42
|
+
import CpTripLegRouteInformation from './CpTripLegRouteInformation.vue'
|
|
43
|
+
import CpTripLegRouteLayover from './CpTripLegRouteLayover.vue'
|
|
44
|
+
import CpTripLegRoutePlace from './CpTripLegRoutePlace.vue'
|
|
45
|
+
|
|
46
|
+
interface Props {
|
|
47
|
+
config: CpTripDetailsConfig
|
|
48
|
+
locale?: string
|
|
49
|
+
previousSegmentArrival: TripDateTime
|
|
50
|
+
segment: FlightSegment
|
|
51
|
+
segmentCount: number
|
|
52
|
+
segmentIndex: number
|
|
53
|
+
stopDuration: TripDuration | null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
57
|
+
locale: 'en',
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const isFirstSegment = computed(() => props.segmentIndex === 0)
|
|
61
|
+
const isLayoverSectionVisible = computed(() => !isFirstSegment.value)
|
|
62
|
+
const origin = computed(() => props.segment.origin)
|
|
63
|
+
const destination = computed(() => props.segment.destination)
|
|
64
|
+
const isDirectFlight = computed(() => props.segmentCount === 1)
|
|
65
|
+
const isLastSegment = computed(() => props.segmentIndex === props.segmentCount - 1)
|
|
66
|
+
|
|
67
|
+
const firstAirportType = computed(() => {
|
|
68
|
+
if (isFirstSegment.value) return FlightStopTypes.ORIGIN
|
|
69
|
+
return FlightStopTypes.LAYOVER_ORIGIN
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const secondAirportType = computed(() => {
|
|
73
|
+
if (isDirectFlight.value || isLastSegment.value) return FlightStopTypes.DESTINATION
|
|
74
|
+
return FlightStopTypes.LAYOVER_DESTINATION
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const departureDate = computed(() =>
|
|
78
|
+
fromGMTToLocal(props.segment.departure_date, props.segment.origin.timezone, props.locale),
|
|
79
|
+
)
|
|
80
|
+
const arrivalDate = computed(() =>
|
|
81
|
+
fromGMTToLocal(props.segment.arrival_date, props.segment.destination.timezone, props.locale),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const dayCountAfterArrival = computed(() => {
|
|
85
|
+
return getDayCountBetweenDates({
|
|
86
|
+
departureDate: departureDate.value,
|
|
87
|
+
arrivalDate: props.previousSegmentArrival,
|
|
88
|
+
isArrival: true,
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const dayCountAfterDeparture = computed(() => {
|
|
93
|
+
return getDayCountBetweenDates({ departureDate: departureDate.value, arrivalDate: arrivalDate.value })
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const segmentDuration = computed(() =>
|
|
97
|
+
getFormattedDuration(getDuration(arrivalDate.value, departureDate.value), props.locale),
|
|
98
|
+
)
|
|
99
|
+
</script>
|
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
|
+
}
|
|
@@ -109,6 +109,36 @@ export const Single: Story = {
|
|
|
109
109
|
}),
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Multi-value selection with a supplier prefix badge. Selected options are
|
|
114
|
+
* rendered as chips.
|
|
115
|
+
*/
|
|
116
|
+
export const Multiple: Story = {
|
|
117
|
+
args: {
|
|
118
|
+
placeholder: 'Select suppliers',
|
|
119
|
+
multiple: true,
|
|
120
|
+
options: supplierOptions,
|
|
121
|
+
trackBy: 'id',
|
|
122
|
+
},
|
|
123
|
+
render: (args) => ({
|
|
124
|
+
components: { CpMultiselect },
|
|
125
|
+
setup() {
|
|
126
|
+
const selectedSuppliers = ref([])
|
|
127
|
+
|
|
128
|
+
return { args, selectedSuppliers }
|
|
129
|
+
},
|
|
130
|
+
template: `
|
|
131
|
+
<div style="padding: 20px; max-width: 25rem;">
|
|
132
|
+
<CpMultiselect v-model="selectedSuppliers" v-bind="args">
|
|
133
|
+
<template #prefix>
|
|
134
|
+
<cp-badge color="magenta" is-square leading-icon="supplier" size="sm" />
|
|
135
|
+
</template>
|
|
136
|
+
</CpMultiselect>
|
|
137
|
+
</div>
|
|
138
|
+
`,
|
|
139
|
+
}),
|
|
140
|
+
}
|
|
141
|
+
|
|
112
142
|
/**
|
|
113
143
|
* Multi-value selection with an async search handler. Selected options are
|
|
114
144
|
* rendered as chips and can be customized through `#tag-leading-icon` and
|
|
@@ -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
|
+
}
|