@citizenplane/pimp 18.22.0 → 18.23.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.22.0",
3
+ "version": "18.23.0",
4
4
  "scripts": {
5
5
  "dev": "storybook dev -p 8081",
6
6
  "build-storybook": "storybook build --output-dir ./docs",
@@ -6,39 +6,7 @@
6
6
  <cp-transition-expand>
7
7
  <div v-if="!applyToAllFlights">
8
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>
9
+ <cp-flight-tabs v-model:active-flight-id="activeFlightId" :flights="tabFlights" />
42
10
  </div>
43
11
  </div>
44
12
  </cp-transition-expand>
@@ -46,7 +14,10 @@
46
14
  </template>
47
15
 
48
16
  <script setup lang="ts">
17
+ import { computed } from 'vue'
18
+
49
19
  import type { CpAncillaryFlightSelectionFlight } from '@/constants/CpAncillaryFlightSelectionTypes'
20
+ import type { CpFlightTabsFlight } from '@/constants/CpFlightTabsTypes'
50
21
 
51
22
  interface Props {
52
23
  flights: CpAncillaryFlightSelectionFlight[]
@@ -64,16 +35,12 @@ const props = withDefaults(defineProps<Props>(), {
64
35
  const applyToAllFlights = defineModel<boolean>('applyToAllFlights', { default: false })
65
36
  const activeFlightId = defineModel<string | undefined>('activeFlightId')
66
37
 
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
- }
38
+ const tabFlights = computed<CpFlightTabsFlight[]>(() =>
39
+ props.flights.map((flight, index) => ({
40
+ ...flight,
41
+ label: index === 0 ? props.outboundLabel : props.returnLabel,
42
+ })),
43
+ )
77
44
  </script>
78
45
 
79
46
  <style lang="scss">
@@ -84,7 +51,6 @@ const handleSelectFlight = (flight: CpAncillaryFlightSelectionFlight) => {
84
51
  padding: var(--cp-spacing-sm);
85
52
  border-radius: var(--cp-radius-xl);
86
53
  background: var(--cp-background-tertiary);
87
- container-type: inline-size;
88
54
  gap: var(--cp-spacing-sm);
89
55
 
90
56
  &__toggle {
@@ -106,87 +72,5 @@ const handleSelectFlight = (flight: CpAncillaryFlightSelectionFlight) => {
106
72
  padding: var(--cp-spacing-lg);
107
73
  gap: var(--cp-spacing-md);
108
74
  }
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
75
  }
192
76
  </style>
@@ -0,0 +1,128 @@
1
+ <template>
2
+ <div class="cpFlightTabs" role="tablist">
3
+ <button
4
+ v-for="flight in flights"
5
+ :key="flight.id"
6
+ :aria-selected="isActiveTab(flight.id)"
7
+ class="cpFlightTabs__tab"
8
+ :class="getTabDynamicClass(flight.id)"
9
+ role="tab"
10
+ type="button"
11
+ @click="handleSelectFlight(flight)"
12
+ >
13
+ <span v-if="flight.label" class="cpFlightTabs__tabLabel">{{ flight.label }}</span>
14
+ <span class="cpFlightTabs__tabRoute">
15
+ <span class="cpFlightTabs__tabRouteText cpFlightTabs__tabRouteText--city">{{ flight.originCity }}</span>
16
+ <span class="cpFlightTabs__tabRouteText cpFlightTabs__tabRouteText--iata">{{ flight.originIata }}</span>
17
+ <cp-icon class="cpFlightTabs__tabIcon" size="16" type="arrow-right" />
18
+ <span class="cpFlightTabs__tabRouteText cpFlightTabs__tabRouteText--city">{{ flight.destinationCity }}</span>
19
+ <span class="cpFlightTabs__tabRouteText cpFlightTabs__tabRouteText--iata">{{ flight.destinationIata }}</span>
20
+ </span>
21
+ </button>
22
+ </div>
23
+ </template>
24
+
25
+ <script setup lang="ts">
26
+ import type { CpFlightTabsFlight } from '@/constants/CpFlightTabsTypes'
27
+
28
+ interface Props {
29
+ flights: CpFlightTabsFlight[]
30
+ }
31
+
32
+ defineProps<Props>()
33
+
34
+ const activeFlightId = defineModel<string | undefined>('activeFlightId')
35
+
36
+ const isActiveTab = (id: string) => id === activeFlightId.value
37
+
38
+ const getTabDynamicClass = (id: string): string => (isActiveTab(id) ? 'cpFlightTabs__tab--isActive' : '')
39
+
40
+ const handleSelectFlight = (flight: CpFlightTabsFlight) => {
41
+ activeFlightId.value = flight.id
42
+ }
43
+ </script>
44
+
45
+ <style lang="scss">
46
+ .cpFlightTabs {
47
+ display: flex;
48
+ width: 100%;
49
+ align-items: center;
50
+ padding: var(--cp-spacing-sm);
51
+ border-radius: var(--cp-radius-md);
52
+ background: var(--cp-background-quaternary);
53
+ container-type: inline-size;
54
+ gap: var(--cp-spacing-sm);
55
+
56
+ &__tab {
57
+ @extend %u-focus-outline;
58
+
59
+ display: flex;
60
+ flex: 1;
61
+ flex-direction: column;
62
+ align-items: center;
63
+ padding: var(--cp-spacing-lg) var(--cp-spacing-xl);
64
+ border-radius: var(--cp-radius-sm);
65
+ gap: var(--cp-spacing-sm-md);
66
+
67
+ &--isActive {
68
+ background: var(--cp-background-primary);
69
+ box-shadow: var(--cp-shadows-xs);
70
+ }
71
+ }
72
+
73
+ &__tabLabel {
74
+ color: var(--cp-text-tertiary);
75
+ font-size: var(--cp-text-size-xs);
76
+ font-weight: 600;
77
+ letter-spacing: 1px;
78
+ line-height: var(--cp-text-xs-line-height);
79
+ padding-inline: var(--cp-spacing-xs);
80
+ text-transform: uppercase;
81
+ }
82
+
83
+ &__tabRoute {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: var(--cp-spacing-sm);
87
+ }
88
+
89
+ &__tabRouteText {
90
+ color: var(--cp-text-secondary);
91
+ font-size: var(--cp-text-size-md);
92
+ font-weight: 500;
93
+ line-height: var(--cp-text-md-line-height);
94
+
95
+ &--iata {
96
+ display: none;
97
+ }
98
+ }
99
+
100
+ &__tabIcon {
101
+ color: var(--cp-text-tertiary);
102
+ }
103
+
104
+ &__tab--isActive &__tabLabel {
105
+ color: var(--cp-text-primary);
106
+ }
107
+
108
+ &__tab--isActive &__tabRouteText {
109
+ color: var(--cp-text-accent-primary);
110
+ }
111
+
112
+ &__tab--isActive &__tabIcon {
113
+ color: var(--cp-foreground-accent-secondary);
114
+ }
115
+
116
+ @container (width < 30rem) {
117
+ .cpFlightTabs__tabRouteText {
118
+ &--city {
119
+ display: none;
120
+ }
121
+
122
+ &--iata {
123
+ display: inline;
124
+ }
125
+ }
126
+ }
127
+ }
128
+ </style>
@@ -36,6 +36,7 @@ import CpDate from './CpDate.vue'
36
36
  import CpDatepicker from './CpDatepicker.vue'
37
37
  import CpDialog from './CpDialog.vue'
38
38
  import CpDynamicDialog from './CpDynamicDialog.vue'
39
+ import CpFlightTabs from './CpFlightTabs.vue'
39
40
  import CpHeading from './CpHeading.vue'
40
41
  import CpIcon from './CpIcon.vue'
41
42
  import CpInput from './CpInput.vue'
@@ -125,6 +126,7 @@ const Components = {
125
126
  CpAncillaryFlightSelection,
126
127
  CpAncillaryItem,
127
128
  CpBasket,
129
+ CpFlightTabs,
128
130
  CpLoader,
129
131
  CpInput,
130
132
  CpText,
@@ -201,6 +203,7 @@ const Pimp: Plugin = {
201
203
 
202
204
  export type { CpAncillaryFlightSelectionFlight } from '@/constants/CpAncillaryFlightSelectionTypes'
203
205
  export type { CpBasketCategory, CpBasketGroup, CpBasketRow } from '@/constants/CpBasketTypes'
206
+ export type { CpFlightTabsFlight } from '@/constants/CpFlightTabsTypes'
204
207
  export type { CpStepperStep } from '@/constants/CpStepperTypes'
205
208
  export type {
206
209
  CpSeatMapConfig,
@@ -259,6 +262,7 @@ export {
259
262
  CpDatepicker,
260
263
  CpDialog,
261
264
  CpDynamicDialog,
265
+ CpFlightTabs,
262
266
  CpHeading,
263
267
  CpIcon,
264
268
  CpInput,
@@ -1,7 +1 @@
1
- export interface CpAncillaryFlightSelectionFlight {
2
- destinationCity?: string
3
- destinationIata?: string
4
- id: string
5
- originCity?: string
6
- originIata?: string
7
- }
1
+ export type { CpFlightTabsFlight as CpAncillaryFlightSelectionFlight } from '@/constants/CpFlightTabsTypes'
@@ -0,0 +1,8 @@
1
+ export interface CpFlightTabsFlight {
2
+ destinationCity?: string
3
+ destinationIata?: string
4
+ id: string
5
+ label?: string
6
+ originCity?: string
7
+ originIata?: string
8
+ }
@@ -0,0 +1,142 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { ref } from 'vue'
3
+
4
+ import type { CpFlightTabsFlight } from '@/constants/CpFlightTabsTypes'
5
+
6
+ import CpFlightTabs from '@/components/CpFlightTabs.vue'
7
+
8
+ import { docLabelStyle } from '@/stories/documentationStyles'
9
+
10
+ const sampleFlights: CpFlightTabsFlight[] = [
11
+ {
12
+ id: 'outbound',
13
+ label: 'Outbound',
14
+ originCity: 'Paris',
15
+ originIata: 'CDG',
16
+ destinationCity: 'New York',
17
+ destinationIata: 'JFK',
18
+ },
19
+ {
20
+ id: 'inbound',
21
+ label: 'Return',
22
+ originCity: 'New York',
23
+ originIata: 'JFK',
24
+ destinationCity: 'Paris',
25
+ destinationIata: 'CDG',
26
+ },
27
+ ]
28
+
29
+ const noLabelFlights: CpFlightTabsFlight[] = sampleFlights.map(({ label: _label, ...flight }) => flight)
30
+
31
+ const threeFlights: CpFlightTabsFlight[] = [
32
+ ...sampleFlights,
33
+ {
34
+ id: 'connecting',
35
+ label: 'Connecting',
36
+ originCity: 'Paris',
37
+ originIata: 'CDG',
38
+ destinationCity: 'Lyon',
39
+ destinationIata: 'LYS',
40
+ },
41
+ ]
42
+
43
+ const meta = {
44
+ title: 'Business/CpFlightTabs',
45
+ component: CpFlightTabs,
46
+ argTypes: {
47
+ flights: {
48
+ control: 'object',
49
+ description: 'Flights shown as tabs',
50
+ },
51
+ },
52
+ } satisfies Meta<typeof CpFlightTabs>
53
+
54
+ export default meta
55
+ type Story = StoryObj<typeof meta>
56
+
57
+ /**
58
+ * Two flights, first one selected. The model is wired with a ref to
59
+ * demonstrate the round trip.
60
+ */
61
+ export const Default: Story = {
62
+ args: {
63
+ flights: sampleFlights,
64
+ },
65
+ render: (args) => ({
66
+ components: { CpFlightTabs },
67
+ setup() {
68
+ const activeFlightId = ref(sampleFlights[0].id)
69
+ return { args, activeFlightId }
70
+ },
71
+ template: `
72
+ <div style="padding: 20px; width: 40rem;">
73
+ <CpFlightTabs v-bind="args" v-model:active-flight-id="activeFlightId" />
74
+ </div>
75
+ `,
76
+ }),
77
+ }
78
+
79
+ /**
80
+ * Flights without a `label`: only the route line is rendered.
81
+ */
82
+ export const NoLabels: Story = {
83
+ args: {
84
+ flights: noLabelFlights,
85
+ },
86
+ render: (args) => ({
87
+ components: { CpFlightTabs },
88
+ setup() {
89
+ const activeFlightId = ref(noLabelFlights[0].id)
90
+ return { args, activeFlightId }
91
+ },
92
+ template: `
93
+ <div style="padding: 20px; width: 40rem;">
94
+ <CpFlightTabs v-bind="args" v-model:active-flight-id="activeFlightId" />
95
+ </div>
96
+ `,
97
+ }),
98
+ }
99
+
100
+ /**
101
+ * Three flights sharing the tab strip.
102
+ */
103
+ export const ThreeFlights: Story = {
104
+ args: {
105
+ flights: threeFlights,
106
+ },
107
+ render: (args) => ({
108
+ components: { CpFlightTabs },
109
+ setup() {
110
+ const activeFlightId = ref(threeFlights[0].id)
111
+ return { args, activeFlightId }
112
+ },
113
+ template: `
114
+ <div style="padding: 20px; width: 40rem;">
115
+ <CpFlightTabs v-bind="args" v-model:active-flight-id="activeFlightId" />
116
+ </div>
117
+ `,
118
+ }),
119
+ }
120
+
121
+ /**
122
+ * At a 358px container width the tabs swap city names for IATA codes.
123
+ */
124
+ export const Mobile: Story = {
125
+ args: {
126
+ flights: sampleFlights,
127
+ },
128
+ parameters: { controls: { disable: true } },
129
+ render: () => ({
130
+ components: { CpFlightTabs },
131
+ setup() {
132
+ const activeFlightId = ref(sampleFlights[0].id)
133
+ return { sampleFlights, activeFlightId, docLabelStyle }
134
+ },
135
+ template: `
136
+ <div style="display: flex; flex-direction: column; gap: 10px; width: 358px;">
137
+ <span :style="docLabelStyle">358px</span>
138
+ <CpFlightTabs :flights="sampleFlights" v-model:active-flight-id="activeFlightId" />
139
+ </div>
140
+ `,
141
+ }),
142
+ }