@citizenplane/pimp 18.20.0 → 18.21.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@citizenplane/pimp",
3
- "version": "18.20.0",
3
+ "version": "18.21.1",
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>
@@ -5,9 +5,7 @@
5
5
  </button>
6
6
  <div class="cpStepperMobile__wrapper">
7
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>
8
+ <cp-text-morph :text="activeStepLabel ?? ''" class="cpStepperMobile__titleInner" />
11
9
  </cp-heading>
12
10
  <div class="cpStepperMobile__steps">
13
11
  <div v-for="step in steps" :key="step.id" class="cpStepperMobile__step" :class="stepActiveClass(step)" />
@@ -17,12 +15,13 @@
17
15
  </template>
18
16
 
19
17
  <script setup lang="ts">
20
- import { computed, ref, watch } from 'vue'
18
+ import { computed } from 'vue'
19
+
20
+ import type { CpStepperStep } from '@/constants/CpStepperTypes'
21
21
 
22
22
  import CpHeading from '@/components/CpHeading.vue'
23
23
  import CpIcon from '@/components/CpIcon.vue'
24
-
25
- import { type CpStepperStep, CpStepperTransitionDirections } from '@/constants/CpStepperTypes'
24
+ import CpTextMorph from '@/components/CpTextMorph.vue'
26
25
 
27
26
  interface Props {
28
27
  activeStepId?: string
@@ -40,24 +39,8 @@ const emit = defineEmits<{ previous: [] }>()
40
39
  const activeStep = computed(() => props.steps.find((step) => step.id === props.activeStepId))
41
40
  const activeStepLabel = computed(() => activeStep.value?.label)
42
41
 
43
- const titleTransitionDirection = ref<CpStepperTransitionDirections>(CpStepperTransitionDirections.FORWARD)
44
- const titleTransitionName = computed(() => `slide-${titleTransitionDirection.value}`)
45
-
46
42
  const stepActiveClass = (step: CpStepperStep) =>
47
43
  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
44
  </script>
62
45
 
63
46
  <style lang="scss">
@@ -122,43 +105,4 @@ watch(
122
105
  }
123
106
  }
124
107
  }
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
108
  </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'
@@ -120,6 +121,7 @@ const Components = {
120
121
  CpCalendar,
121
122
  CpAlert,
122
123
  CpAncillaryCategory,
124
+ CpAncillaryFlightSelection,
123
125
  CpAncillaryItem,
124
126
  CpLoader,
125
127
  CpInput,
@@ -195,6 +197,7 @@ const Pimp: Plugin = {
195
197
  },
196
198
  }
197
199
 
200
+ export type { CpAncillaryFlightSelectionFlight } from '@/constants/CpAncillaryFlightSelectionTypes'
198
201
  export type { CpStepperStep } from '@/constants/CpStepperTypes'
199
202
  export type {
200
203
  CpSeatMapConfig,
@@ -219,7 +222,6 @@ export type {
219
222
 
220
223
  export { CpAncillaryCategoryTypes, isSwitchCategory } from '@/constants/CpAncillaryCategoryTypes'
221
224
  export { CpAncillaryItemTypes } from '@/constants/CpAncillaryItemTypes'
222
- export { CpStepperTransitionDirections } from '@/constants/CpStepperTypes'
223
225
  export {
224
226
  CpSeatMapTransitionDirections,
225
227
  DEFAULT_CP_SEAT_MAP_CONFIG,
@@ -238,6 +240,7 @@ export {
238
240
  CpAirlineLogo,
239
241
  CpAlert,
240
242
  CpAncillaryCategory,
243
+ CpAncillaryFlightSelection,
241
244
  CpAncillaryItem,
242
245
  CpBadge,
243
246
  CpBottomSheet,
@@ -0,0 +1,7 @@
1
+ export interface CpAncillaryFlightSelectionFlight {
2
+ destinationCity?: string
3
+ destinationIata?: string
4
+ id: string
5
+ originCity?: string
6
+ originIata?: string
7
+ }
@@ -2,8 +2,3 @@ export interface CpStepperStep {
2
2
  id: string
3
3
  label: string
4
4
  }
5
-
6
- export enum CpStepperTransitionDirections {
7
- BACKWARD = 'backward',
8
- FORWARD = 'forward',
9
- }
@@ -0,0 +1,135 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { ref } from 'vue'
3
+
4
+ import type { CpAncillaryFlightSelectionFlight } from '@/constants/CpAncillaryFlightSelectionTypes'
5
+
6
+ import CpAncillaryFlightSelection from '@/components/CpAncillaryFlightSelection.vue'
7
+
8
+ import { docLabelStyle } from '@/stories/documentationStyles'
9
+
10
+ const sampleFlights: CpAncillaryFlightSelectionFlight[] = [
11
+ {
12
+ id: 'outbound',
13
+ originCity: 'Paris',
14
+ originIata: 'CDG',
15
+ destinationCity: 'New York',
16
+ destinationIata: 'JFK',
17
+ },
18
+ {
19
+ id: 'inbound',
20
+ originCity: 'New York',
21
+ originIata: 'JFK',
22
+ destinationCity: 'Paris',
23
+ destinationIata: 'CDG',
24
+ },
25
+ ]
26
+
27
+ const meta = {
28
+ title: 'Business/CpAncillaryFlightSelection',
29
+ component: CpAncillaryFlightSelection,
30
+ argTypes: {
31
+ label: {
32
+ control: 'text',
33
+ description: 'Label of the "apply to all flights" switch',
34
+ },
35
+ outboundLabel: {
36
+ control: 'text',
37
+ description: 'Label of the first flight tab',
38
+ },
39
+ returnLabel: {
40
+ control: 'text',
41
+ description: 'Label of the second flight tab',
42
+ },
43
+ flights: {
44
+ control: 'object',
45
+ description: 'Flights shown as tabs when the switch is off',
46
+ },
47
+ },
48
+ } satisfies Meta<typeof CpAncillaryFlightSelection>
49
+
50
+ export default meta
51
+ type Story = StoryObj<typeof meta>
52
+
53
+ /**
54
+ * Switch off, tab strip visible, first flight selected. The models are wired
55
+ * with refs to demonstrate the round trip.
56
+ */
57
+ export const Default: Story = {
58
+ args: {
59
+ flights: sampleFlights,
60
+ label: 'Apply to all flights',
61
+ outboundLabel: 'Outbound',
62
+ returnLabel: 'Return',
63
+ },
64
+ render: (args) => ({
65
+ components: { CpAncillaryFlightSelection },
66
+ setup() {
67
+ const applyToAllFlights = ref(false)
68
+ const activeFlightId = ref(sampleFlights[0].id)
69
+ return { args, applyToAllFlights, activeFlightId }
70
+ },
71
+ template: `
72
+ <div style="padding: 20px; width: 40rem;">
73
+ <CpAncillaryFlightSelection
74
+ v-bind="args"
75
+ v-model:apply-to-all-flights="applyToAllFlights"
76
+ v-model:active-flight-id="activeFlightId"
77
+ />
78
+ </div>
79
+ `,
80
+ }),
81
+ }
82
+
83
+ /**
84
+ * Switch on: the tab strip collapses away.
85
+ */
86
+ export const ApplyToAllFlights: Story = {
87
+ args: {
88
+ ...Default.args,
89
+ },
90
+ render: (args) => ({
91
+ components: { CpAncillaryFlightSelection },
92
+ setup() {
93
+ const applyToAllFlights = ref(true)
94
+ const activeFlightId = ref(sampleFlights[0].id)
95
+ return { args, applyToAllFlights, activeFlightId }
96
+ },
97
+ template: `
98
+ <div style="padding: 20px; width: 40rem;">
99
+ <CpAncillaryFlightSelection
100
+ v-bind="args"
101
+ v-model:apply-to-all-flights="applyToAllFlights"
102
+ v-model:active-flight-id="activeFlightId"
103
+ />
104
+ </div>
105
+ `,
106
+ }),
107
+ }
108
+
109
+ /**
110
+ * At a 358px container width the tabs swap city names for IATA codes.
111
+ */
112
+ export const Mobile: Story = {
113
+ args: {
114
+ ...Default.args,
115
+ },
116
+ parameters: { controls: { disable: true } },
117
+ render: () => ({
118
+ components: { CpAncillaryFlightSelection },
119
+ setup() {
120
+ const applyToAllFlights = ref(false)
121
+ const activeFlightId = ref(sampleFlights[0].id)
122
+ return { sampleFlights, applyToAllFlights, activeFlightId, docLabelStyle }
123
+ },
124
+ template: `
125
+ <div style="display: flex; flex-direction: column; gap: 10px; width: 358px;">
126
+ <span :style="docLabelStyle">358px</span>
127
+ <CpAncillaryFlightSelection
128
+ :flights="sampleFlights"
129
+ v-model:apply-to-all-flights="applyToAllFlights"
130
+ v-model:active-flight-id="activeFlightId"
131
+ />
132
+ </div>
133
+ `,
134
+ }),
135
+ }
@@ -53,8 +53,8 @@ export const Default: Story = {
53
53
  }
54
54
 
55
55
  /**
56
- * Previous/Next move the active step index so the title's
57
- * slide-forward / slide-backward transition plays in each direction.
56
+ * Previous/Next move the active step index so the title morphs
57
+ * from the previous step name to the new one.
58
58
  */
59
59
  export const Interactive: Story = {
60
60
  args: { ...Default.args },