@citizenplane/pimp 18.19.10 → 18.21.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@citizenplane/pimp",
3
- "version": "18.19.10",
3
+ "version": "18.21.0",
4
4
  "scripts": {
5
5
  "dev": "storybook dev -p 8081",
6
6
  "build-storybook": "storybook build --output-dir ./docs",
@@ -0,0 +1,192 @@
1
+ <template>
2
+ <div class="cpAncillaryFlightSelection">
3
+ <div class="cpAncillaryFlightSelection__toggle">
4
+ <cp-switch v-model="applyToAllFlights" color="accent" :label="label" reverse-label />
5
+ </div>
6
+ <cp-transition-expand>
7
+ <div v-if="!applyToAllFlights">
8
+ <div class="cpAncillaryFlightSelection__tabsWrapper">
9
+ <div class="cpAncillaryFlightSelection__tabs" role="tablist">
10
+ <button
11
+ v-for="(flight, index) in flights"
12
+ :key="flight.id"
13
+ :aria-selected="isActiveTab(flight.id)"
14
+ class="cpAncillaryFlightSelection__tab"
15
+ :class="getTabDynamicClass(flight.id)"
16
+ role="tab"
17
+ type="button"
18
+ @click="handleSelectFlight(flight)"
19
+ >
20
+ <span class="cpAncillaryFlightSelection__tabLabel">{{ getTabLabel(index) }}</span>
21
+ <span class="cpAncillaryFlightSelection__tabRoute">
22
+ <span
23
+ class="cpAncillaryFlightSelection__tabRouteText cpAncillaryFlightSelection__tabRouteText--city"
24
+ >{{ flight.originCity }}</span
25
+ >
26
+ <span
27
+ class="cpAncillaryFlightSelection__tabRouteText cpAncillaryFlightSelection__tabRouteText--iata"
28
+ >{{ flight.originIata }}</span
29
+ >
30
+ <cp-icon class="cpAncillaryFlightSelection__tabIcon" size="16" type="arrow-right" />
31
+ <span
32
+ class="cpAncillaryFlightSelection__tabRouteText cpAncillaryFlightSelection__tabRouteText--city"
33
+ >{{ flight.destinationCity }}</span
34
+ >
35
+ <span
36
+ class="cpAncillaryFlightSelection__tabRouteText cpAncillaryFlightSelection__tabRouteText--iata"
37
+ >{{ flight.destinationIata }}</span
38
+ >
39
+ </span>
40
+ </button>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </cp-transition-expand>
45
+ </div>
46
+ </template>
47
+
48
+ <script setup lang="ts">
49
+ import type { CpAncillaryFlightSelectionFlight } from '@/constants/CpAncillaryFlightSelectionTypes'
50
+
51
+ interface Props {
52
+ flights: CpAncillaryFlightSelectionFlight[]
53
+ label?: string
54
+ outboundLabel?: string
55
+ returnLabel?: string
56
+ }
57
+
58
+ const props = withDefaults(defineProps<Props>(), {
59
+ label: 'Apply to all flights',
60
+ outboundLabel: 'Outbound',
61
+ returnLabel: 'Return',
62
+ })
63
+
64
+ const applyToAllFlights = defineModel<boolean>('applyToAllFlights', { default: false })
65
+ const activeFlightId = defineModel<string | undefined>('activeFlightId')
66
+
67
+ const isActiveTab = (id: string) => id === activeFlightId.value
68
+
69
+ const getTabDynamicClass = (id: string): string =>
70
+ isActiveTab(id) ? 'cpAncillaryFlightSelection__tab--isActive' : ''
71
+
72
+ const getTabLabel = (index: number): string => (index === 0 ? props.outboundLabel : props.returnLabel)
73
+
74
+ const handleSelectFlight = (flight: CpAncillaryFlightSelectionFlight) => {
75
+ activeFlightId.value = flight.id
76
+ }
77
+ </script>
78
+
79
+ <style lang="scss">
80
+ .cpAncillaryFlightSelection {
81
+ display: flex;
82
+ flex-direction: column;
83
+ justify-content: center;
84
+ padding: var(--cp-spacing-sm);
85
+ border-radius: var(--cp-radius-xl);
86
+ background: var(--cp-background-tertiary);
87
+ container-type: inline-size;
88
+ gap: var(--cp-spacing-sm);
89
+
90
+ &__toggle {
91
+ padding: var(--cp-spacing-lg) var(--cp-spacing-xl) var(--cp-spacing-lg) var(--cp-spacing-lg);
92
+ border-radius: var(--cp-radius-lg);
93
+ background: var(--cp-background-primary);
94
+
95
+ .cpSwitch__label {
96
+ color: var(--cp-text-secondary);
97
+ font-size: var(--cp-text-size-md);
98
+ font-weight: 500;
99
+ line-height: var(--cp-text-md-line-height);
100
+ }
101
+ }
102
+
103
+ &__tabsWrapper {
104
+ display: flex;
105
+ align-items: center;
106
+ padding: var(--cp-spacing-lg);
107
+ gap: var(--cp-spacing-md);
108
+ }
109
+
110
+ &__tabs {
111
+ display: flex;
112
+ width: 100%;
113
+ align-items: center;
114
+ padding: var(--cp-spacing-sm);
115
+ border-radius: var(--cp-radius-md);
116
+ background: var(--cp-background-quaternary);
117
+ gap: var(--cp-spacing-sm);
118
+ }
119
+
120
+ &__tab {
121
+ @extend %u-focus-outline;
122
+
123
+ display: flex;
124
+ flex: 1;
125
+ flex-direction: column;
126
+ align-items: center;
127
+ padding: var(--cp-spacing-lg) var(--cp-spacing-xl);
128
+ border-radius: var(--cp-radius-sm);
129
+ gap: var(--cp-spacing-sm-md);
130
+
131
+ &--isActive {
132
+ background: var(--cp-background-primary);
133
+ box-shadow: var(--cp-shadows-xs);
134
+ }
135
+ }
136
+
137
+ &__tabLabel {
138
+ color: var(--cp-text-tertiary);
139
+ font-size: var(--cp-text-size-xs);
140
+ font-weight: 600;
141
+ letter-spacing: 1px;
142
+ line-height: var(--cp-text-xs-line-height);
143
+ padding-inline: var(--cp-spacing-xs);
144
+ text-transform: uppercase;
145
+ }
146
+
147
+ &__tabRoute {
148
+ display: flex;
149
+ align-items: center;
150
+ gap: var(--cp-spacing-sm);
151
+ }
152
+
153
+ &__tabRouteText {
154
+ color: var(--cp-text-secondary);
155
+ font-size: var(--cp-text-size-md);
156
+ font-weight: 500;
157
+ line-height: var(--cp-text-md-line-height);
158
+
159
+ &--iata {
160
+ display: none;
161
+ }
162
+ }
163
+
164
+ &__tabIcon {
165
+ color: var(--cp-text-tertiary);
166
+ }
167
+
168
+ &__tab--isActive &__tabLabel {
169
+ color: var(--cp-text-primary);
170
+ }
171
+
172
+ &__tab--isActive &__tabRouteText {
173
+ color: var(--cp-text-accent-primary);
174
+ }
175
+
176
+ &__tab--isActive &__tabIcon {
177
+ color: var(--cp-foreground-accent-secondary);
178
+ }
179
+
180
+ @container (width < 30rem) {
181
+ .cpAncillaryFlightSelection__tabRouteText {
182
+ &--city {
183
+ display: none;
184
+ }
185
+
186
+ &--iata {
187
+ display: inline;
188
+ }
189
+ }
190
+ }
191
+ }
192
+ </style>
@@ -0,0 +1,136 @@
1
+ <template>
2
+ <div class="cpStepper">
3
+ <div class="cpStepper__wrapper">
4
+ <div v-for="(step, stepIndex) in steps" :key="step.id" class="cpStepper__stepWrapper">
5
+ <button
6
+ class="cpStepper__step"
7
+ :class="stepActiveClass(step)"
8
+ :disabled="isStepDisabled(stepIndex)"
9
+ type="button"
10
+ @click="handleStepClick(step)"
11
+ >
12
+ <span class="cpStepper__stepNumber">{{ stepIndex + 1 }}</span>
13
+ <span class="cpStepper__stepTitle">{{ step.label }}</span>
14
+ </button>
15
+ <cp-icon v-if="isNotLastStep(stepIndex)" class="cpStepper__chevron" size="16" type="chevron-right" />
16
+ </div>
17
+ </div>
18
+ </div>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import type { CpStepperStep } from '@/constants/CpStepperTypes'
23
+
24
+ import CpIcon from '@/components/CpIcon.vue'
25
+
26
+ interface Props {
27
+ activeStepId?: string
28
+ reachedStepIndex?: number
29
+ steps: CpStepperStep[]
30
+ }
31
+
32
+ const props = withDefaults(defineProps<Props>(), {
33
+ activeStepId: undefined,
34
+ reachedStepIndex: 0,
35
+ })
36
+
37
+ const emit = defineEmits<{ select: [id: string] }>()
38
+
39
+ const isNotLastStep = (stepIndex: number) => stepIndex < props.steps.length - 1
40
+
41
+ const isStepDisabled = (stepIndex: number) => stepIndex > props.reachedStepIndex
42
+
43
+ const stepActiveClass = (step: CpStepperStep) => (step.id === props.activeStepId ? 'cpStepper__step--isActive' : '')
44
+
45
+ const handleStepClick = (step: CpStepperStep) => emit('select', step.id)
46
+ </script>
47
+
48
+ <style lang="scss">
49
+ .cpStepper {
50
+ min-width: 0;
51
+ flex: 1;
52
+ container-type: inline-size;
53
+
54
+ &__wrapper,
55
+ &__stepWrapper {
56
+ display: flex;
57
+ align-items: center;
58
+ justify-content: center;
59
+ gap: var(--cp-spacing-md);
60
+ }
61
+
62
+ &__stepWrapper {
63
+ @extend %u-text-ellipsis;
64
+ }
65
+
66
+ &__step {
67
+ @extend %u-text-ellipsis;
68
+
69
+ display: flex;
70
+ align-items: center;
71
+ padding: fn.px-to-rem(6);
72
+ border: fn.px-to-rem(1) solid var(--cp-border-soft);
73
+ border-radius: fn.px-to-rem(50);
74
+ color: var(--cp-text-secondary);
75
+ gap: var(--cp-spacing-sm);
76
+ outline-offset: fn.px-to-rem(-2);
77
+
78
+ @container (width < 60.5rem) {
79
+ padding: var(--cp-spacing-sm);
80
+ }
81
+
82
+ &--isActive {
83
+ border-color: transparent;
84
+ background-color: var(--cp-background-accent-secondary);
85
+ color: var(--cp-text-accent-primary);
86
+ }
87
+
88
+ &:not(:disabled) {
89
+ @extend %u-focus-outline;
90
+
91
+ border-radius: fn.px-to-rem(50);
92
+ }
93
+
94
+ &:disabled {
95
+ cursor: not-allowed;
96
+ }
97
+ }
98
+
99
+ &__stepNumber {
100
+ @include mx.square-sizing(20);
101
+
102
+ display: flex;
103
+ flex-shrink: 0;
104
+ align-items: center;
105
+ justify-content: center;
106
+ border-radius: 50%;
107
+ background-color: var(--cp-background-secondary);
108
+ font-size: var(--cp-text-size-xs);
109
+ font-weight: 500;
110
+ line-height: var(--cp-text-xs-line-height);
111
+ }
112
+
113
+ &__step--isActive &__stepNumber {
114
+ background-color: var(--cp-background-accent-solid);
115
+ color: var(--cp-text-white);
116
+ }
117
+
118
+ &__stepTitle {
119
+ @extend %u-text-ellipsis;
120
+
121
+ font-size: var(--cp-text-size-sm);
122
+ font-weight: 500;
123
+ line-height: var(--cp-text-sm-line-height);
124
+ padding-inline: var(--cp-spacing-sm);
125
+
126
+ @container (width < 60.5rem) {
127
+ display: none;
128
+ }
129
+ }
130
+
131
+ &__chevron {
132
+ flex-shrink: 0;
133
+ color: var(--cp-foreground-quaternary);
134
+ }
135
+ }
136
+ </style>
@@ -0,0 +1,164 @@
1
+ <template>
2
+ <div class="cpStepperMobile">
3
+ <button v-if="hasPreviousButton" class="cpStepperMobile__back" type="button" @click="emit('previous')">
4
+ <cp-icon size="24" type="chevron-left" />
5
+ </button>
6
+ <div class="cpStepperMobile__wrapper">
7
+ <cp-heading class="cpStepperMobile__title" heading-level="h3" :size="600">
8
+ <transition mode="out-in" :name="titleTransitionName">
9
+ <span :key="activeStepLabel" class="cpStepperMobile__titleInner">{{ activeStepLabel }}</span>
10
+ </transition>
11
+ </cp-heading>
12
+ <div class="cpStepperMobile__steps">
13
+ <div v-for="step in steps" :key="step.id" class="cpStepperMobile__step" :class="stepActiveClass(step)" />
14
+ </div>
15
+ </div>
16
+ </div>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import { computed, ref, watch } from 'vue'
21
+
22
+ import CpHeading from '@/components/CpHeading.vue'
23
+ import CpIcon from '@/components/CpIcon.vue'
24
+
25
+ import { type CpStepperStep, CpStepperTransitionDirections } from '@/constants/CpStepperTypes'
26
+
27
+ interface Props {
28
+ activeStepId?: string
29
+ hasPreviousButton?: boolean
30
+ steps: CpStepperStep[]
31
+ }
32
+
33
+ const props = withDefaults(defineProps<Props>(), {
34
+ activeStepId: undefined,
35
+ hasPreviousButton: false,
36
+ })
37
+
38
+ const emit = defineEmits<{ previous: [] }>()
39
+
40
+ const activeStep = computed(() => props.steps.find((step) => step.id === props.activeStepId))
41
+ const activeStepLabel = computed(() => activeStep.value?.label)
42
+
43
+ const titleTransitionDirection = ref<CpStepperTransitionDirections>(CpStepperTransitionDirections.FORWARD)
44
+ const titleTransitionName = computed(() => `slide-${titleTransitionDirection.value}`)
45
+
46
+ const stepActiveClass = (step: CpStepperStep) =>
47
+ step.id === props.activeStepId ? 'cpStepperMobile__step--isActive' : ''
48
+
49
+ watch(
50
+ () => props.activeStepId,
51
+ (newStepId, oldStepId) => {
52
+ if (!newStepId || !oldStepId) return
53
+
54
+ const newIndex = props.steps.findIndex((step) => step.id === newStepId)
55
+ const oldIndex = props.steps.findIndex((step) => step.id === oldStepId)
56
+
57
+ titleTransitionDirection.value =
58
+ newIndex > oldIndex ? CpStepperTransitionDirections.BACKWARD : CpStepperTransitionDirections.FORWARD
59
+ },
60
+ )
61
+ </script>
62
+
63
+ <style lang="scss">
64
+ .cpStepperMobile {
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: center;
68
+
69
+ &__back {
70
+ @extend %u-focus-outline;
71
+
72
+ position: relative;
73
+ z-index: 2;
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ color: var(--cp-foreground-accent-secondary);
78
+ }
79
+
80
+ &__wrapper {
81
+ display: flex;
82
+ flex: 1;
83
+ flex-direction: column;
84
+ align-items: center;
85
+ justify-content: center;
86
+ padding: var(--cp-spacing-sm) var(--cp-spacing-xl);
87
+ }
88
+
89
+ &__title {
90
+ overflow: hidden;
91
+ font-size: var(--cp-text-size-lg);
92
+ font-weight: 600;
93
+ line-height: var(--cp-text-lg-line-height);
94
+ }
95
+
96
+ &__titleInner {
97
+ display: flex;
98
+ }
99
+
100
+ &__steps {
101
+ display: flex;
102
+ width: 100%;
103
+ max-width: fn.px-to-rem(192);
104
+ min-height: fn.px-to-rem(20);
105
+ align-items: center;
106
+ justify-content: center;
107
+ gap: var(--cp-spacing-md);
108
+ }
109
+
110
+ &__step {
111
+ height: fn.px-to-rem(4);
112
+ flex: 0 0 fn.px-to-rem(24);
113
+ border-radius: fn.px-to-rem(4);
114
+ background-color: var(--cp-utility-accent-100);
115
+ transition:
116
+ flex-basis 500ms var(--cp-easing-elastic),
117
+ background-color 500ms ease;
118
+
119
+ &--isActive {
120
+ flex-basis: fn.px-to-rem(48);
121
+ background-color: var(--cp-background-accent-solid);
122
+ }
123
+ }
124
+ }
125
+
126
+ .slide-backward,
127
+ .slide-forward {
128
+ &-enter-active,
129
+ &-leave-active {
130
+ transition:
131
+ transform 200ms ease,
132
+ opacity 200ms ease;
133
+ }
134
+
135
+ &-enter-from,
136
+ &-leave-to {
137
+ opacity: 0;
138
+ }
139
+
140
+ &-enter-to {
141
+ transform: translate3d(0, 0, 0);
142
+ }
143
+ }
144
+
145
+ .slide-forward {
146
+ &-enter-from {
147
+ transform: translate3d(3vw, 0, 0);
148
+ }
149
+
150
+ &-leave-to {
151
+ transform: translate3d(-3vw, 0, 0);
152
+ }
153
+ }
154
+
155
+ .slide-backward {
156
+ &-enter-from {
157
+ transform: translate3d(-3vw, 0, 0);
158
+ }
159
+
160
+ &-leave-to {
161
+ transform: translate3d(3vw, 0, 0);
162
+ }
163
+ }
164
+ </style>
@@ -21,6 +21,7 @@ import CpAccordionGroup from './CpAccordionGroup.vue'
21
21
  import CpAirlineLogo from './CpAirlineLogo.vue'
22
22
  import CpAlert from './CpAlert.vue'
23
23
  import CpAncillaryCategory from './CpAncillaryCategory.vue'
24
+ import CpAncillaryFlightSelection from './CpAncillaryFlightSelection.vue'
24
25
  import CpAncillaryItem from './CpAncillaryItem.vue'
25
26
  import CpBadge from './CpBadge.vue'
26
27
  import CpBottomSheet from './CpBottomSheet.vue'
@@ -60,6 +61,8 @@ import CpSegmentedControl from './CpSegmentedControl.vue'
60
61
  import CpSelect from './CpSelect.vue'
61
62
  import CpSelectableButton from './CpSelectableButton.vue'
62
63
  import CpSelectMenu from './CpSelectMenu.vue'
64
+ import CpStepper from './CpStepper.vue'
65
+ import CpStepperMobile from './CpStepperMobile.vue'
63
66
  import CpSwitch from './CpSwitch.vue'
64
67
  import CpTable from './CpTable.vue'
65
68
  import CpTableColumnEditor from './CpTableColumnEditor.vue'
@@ -118,6 +121,7 @@ const Components = {
118
121
  CpCalendar,
119
122
  CpAlert,
120
123
  CpAncillaryCategory,
124
+ CpAncillaryFlightSelection,
121
125
  CpAncillaryItem,
122
126
  CpLoader,
123
127
  CpInput,
@@ -131,6 +135,8 @@ const Components = {
131
135
  CpRadio,
132
136
  CpRadioGroup,
133
137
  CpSelectableButton,
138
+ CpStepper,
139
+ CpStepperMobile,
134
140
  CpSwitch,
135
141
  CpSegmentedControl,
136
142
  CpTable,
@@ -191,6 +197,8 @@ const Pimp: Plugin = {
191
197
  },
192
198
  }
193
199
 
200
+ export type { CpAncillaryFlightSelectionFlight } from '@/constants/CpAncillaryFlightSelectionTypes'
201
+ export type { CpStepperStep } from '@/constants/CpStepperTypes'
194
202
  export type {
195
203
  CpSeatMapConfig,
196
204
  CpSeatMapSeatSelection,
@@ -214,6 +222,7 @@ export type {
214
222
 
215
223
  export { CpAncillaryCategoryTypes, isSwitchCategory } from '@/constants/CpAncillaryCategoryTypes'
216
224
  export { CpAncillaryItemTypes } from '@/constants/CpAncillaryItemTypes'
225
+ export { CpStepperTransitionDirections } from '@/constants/CpStepperTypes'
217
226
  export {
218
227
  CpSeatMapTransitionDirections,
219
228
  DEFAULT_CP_SEAT_MAP_CONFIG,
@@ -232,6 +241,7 @@ export {
232
241
  CpAirlineLogo,
233
242
  CpAlert,
234
243
  CpAncillaryCategory,
244
+ CpAncillaryFlightSelection,
235
245
  CpAncillaryItem,
236
246
  CpBadge,
237
247
  CpBottomSheet,
@@ -272,6 +282,8 @@ export {
272
282
  CpSelect,
273
283
  CpSelectableButton,
274
284
  CpSelectMenu,
285
+ CpStepper,
286
+ CpStepperMobile,
275
287
  CpSwitch,
276
288
  CpTable,
277
289
  CpTableColumnEditor,
@@ -0,0 +1,7 @@
1
+ export interface CpAncillaryFlightSelectionFlight {
2
+ destinationCity?: string
3
+ destinationIata?: string
4
+ id: string
5
+ originCity?: string
6
+ originIata?: string
7
+ }
@@ -0,0 +1,9 @@
1
+ export interface CpStepperStep {
2
+ id: string
3
+ label: string
4
+ }
5
+
6
+ export enum CpStepperTransitionDirections {
7
+ BACKWARD = 'backward',
8
+ FORWARD = 'forward',
9
+ }