@citizenplane/pimp 18.22.0 → 18.23.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.22.0",
3
+ "version": "18.23.1",
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>
@@ -1,24 +1,30 @@
1
1
  <template>
2
2
  <div class="cpStepper">
3
3
  <div class="cpStepper__wrapper">
4
- <div v-for="(step, stepIndex) in steps" :key="step.id" class="cpStepper__stepWrapper">
4
+ <div v-for="stepItem in stepItems" :key="stepItem.id" class="cpStepper__stepWrapper">
5
5
  <button
6
+ :aria-label="stepItem.label"
6
7
  class="cpStepper__step"
7
- :class="stepActiveClass(step)"
8
- :disabled="isStepDisabled(stepIndex)"
8
+ :class="stepItem.className"
9
+ :disabled="stepItem.isDisabled"
9
10
  type="button"
10
- @click="handleStepClick(step)"
11
+ @click="handleStepClick(stepItem.id)"
11
12
  >
12
- <span class="cpStepper__stepNumber">{{ stepIndex + 1 }}</span>
13
- <span class="cpStepper__stepTitle">{{ step.label }}</span>
13
+ <span class="cpStepper__stepNumber">
14
+ <cp-icon v-if="stepItem.isCompleted" size="12" type="check" />
15
+ <template v-else>{{ stepItem.number }}</template>
16
+ </span>
17
+ <span class="cpStepper__stepTitle">{{ stepItem.label }}</span>
14
18
  </button>
15
- <cp-icon v-if="isNotLastStep(stepIndex)" class="cpStepper__chevron" size="16" type="chevron-right" />
19
+ <cp-icon v-if="stepItem.isNotLast" class="cpStepper__chevron" size="16" type="chevron-right" />
16
20
  </div>
17
21
  </div>
18
22
  </div>
19
23
  </template>
20
24
 
21
25
  <script setup lang="ts">
26
+ import { computed } from 'vue'
27
+
22
28
  import type { CpStepperStep } from '@/constants/CpStepperTypes'
23
29
 
24
30
  import CpIcon from '@/components/CpIcon.vue'
@@ -36,13 +42,32 @@ const props = withDefaults(defineProps<Props>(), {
36
42
 
37
43
  const emit = defineEmits<{ select: [id: string] }>()
38
44
 
39
- const isNotLastStep = (stepIndex: number) => stepIndex < props.steps.length - 1
45
+ const activeStepIndex = computed(() => props.steps.findIndex((step) => step.id === props.activeStepId))
46
+
47
+ const isStepCompleted = (stepIndex: number) => activeStepIndex.value > -1 && stepIndex < activeStepIndex.value
40
48
 
41
- const isStepDisabled = (stepIndex: number) => stepIndex > props.reachedStepIndex
49
+ const stepClassName = (step: CpStepperStep, stepIndex: number) => {
50
+ return {
51
+ 'cpStepper__step--isActive': step.id === props.activeStepId,
52
+ 'cpStepper__step--isCompleted': isStepCompleted(stepIndex),
53
+ }
54
+ }
42
55
 
43
- const stepActiveClass = (step: CpStepperStep) => (step.id === props.activeStepId ? 'cpStepper__step--isActive' : '')
56
+ const stepItems = computed(() => {
57
+ return props.steps.map((step, stepIndex) => {
58
+ return {
59
+ className: stepClassName(step, stepIndex),
60
+ id: step.id,
61
+ isCompleted: isStepCompleted(stepIndex),
62
+ isDisabled: stepIndex > props.reachedStepIndex,
63
+ isNotLast: stepIndex < props.steps.length - 1,
64
+ label: step.label,
65
+ number: stepIndex + 1,
66
+ }
67
+ })
68
+ })
44
69
 
45
- const handleStepClick = (step: CpStepperStep) => emit('select', step.id)
70
+ const handleStepClick = (stepId: string) => emit('select', stepId)
46
71
  </script>
47
72
 
48
73
  <style lang="scss">
@@ -85,6 +110,12 @@ const handleStepClick = (step: CpStepperStep) => emit('select', step.id)
85
110
  color: var(--cp-text-accent-primary);
86
111
  }
87
112
 
113
+ &--isCompleted {
114
+ border-color: transparent;
115
+ background-color: var(--cp-background-primary);
116
+ color: var(--cp-text-primary);
117
+ }
118
+
88
119
  &:not(:disabled) {
89
120
  @extend %u-focus-outline;
90
121
 
@@ -115,6 +146,11 @@ const handleStepClick = (step: CpStepperStep) => emit('select', step.id)
115
146
  color: var(--cp-text-white);
116
147
  }
117
148
 
149
+ &__step--isCompleted &__stepNumber {
150
+ background-color: var(--cp-background-success-primary);
151
+ color: var(--cp-foreground-success-primary);
152
+ }
153
+
118
154
  &__stepTitle {
119
155
  @extend %u-text-ellipsis;
120
156
 
@@ -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
+ }
@@ -37,6 +37,10 @@ export default meta
37
37
  type Story = StoryObj<typeof meta>
38
38
 
39
39
  /**
40
+ * Three visual states coexist: steps before the active one render as
41
+ * completed (white pill, green check badge, dark label), the active one keeps
42
+ * its accent pill, and the following ones stay upcoming (bordered, grey).
43
+ *
40
44
  * Clicking an enabled step updates the active step locally, the way a
41
45
  * consumer would react to `select` by navigating.
42
46
  */
@@ -65,10 +69,55 @@ export const Default: Story = {
65
69
  }),
66
70
  }
67
71
 
72
+ /**
73
+ * Three steps already behind the active one: each renders completed, with the
74
+ * check badge replacing its number. Completed steps stay clickable.
75
+ */
76
+ export const Completed: Story = {
77
+ args: {
78
+ steps,
79
+ activeStepId: 'extras',
80
+ reachedStepIndex: 3,
81
+ },
82
+ render: (args) => ({
83
+ components: { CpStepper },
84
+ setup() {
85
+ return { args }
86
+ },
87
+ template: `
88
+ <div style="width: 70rem;">
89
+ <CpStepper v-bind="args" />
90
+ </div>
91
+ `,
92
+ }),
93
+ }
94
+
95
+ /**
96
+ * The funnel's last step is active, so every preceding step is completed.
97
+ */
98
+ export const LastStep: Story = {
99
+ args: {
100
+ steps,
101
+ activeStepId: 'confirmation',
102
+ reachedStepIndex: 4,
103
+ },
104
+ render: (args) => ({
105
+ components: { CpStepper },
106
+ setup() {
107
+ return { args }
108
+ },
109
+ template: `
110
+ <div style="width: 70rem;">
111
+ <CpStepper v-bind="args" />
112
+ </div>
113
+ `,
114
+ }),
115
+ }
116
+
68
117
  /**
69
118
  * Below the container width threshold (60.5rem, matching online-checkin's
70
119
  * former `$breakpoint-large-tablet`), step titles collapse and only the
71
- * numbered pills remain.
120
+ * numbered pills remain, the completed ones showing their check badge.
72
121
  */
73
122
  export const Narrow: Story = {
74
123
  args: { ...Default.args },