@citizenplane/pimp 18.21.1 → 18.22.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/dist/components/CpBasket.vue.d.ts +25 -0
- package/dist/components/CpBasket.vue.d.ts.map +1 -0
- package/dist/components/CpQuantityCounter.vue.d.ts.map +1 -1
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/constants/CpBasketTypes.d.ts +16 -0
- package/dist/constants/CpBasketTypes.d.ts.map +1 -0
- package/dist/pimp.es.js +1604 -1532
- package/dist/pimp.umd.js +50 -50
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/CpBasket.vue +271 -0
- package/src/components/CpQuantityCounter.vue +6 -0
- package/src/components/index.ts +4 -0
- package/src/constants/CpBasketTypes.ts +17 -0
- package/src/stories/CpBasket.stories.ts +323 -0
package/package.json
CHANGED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cpBasket">
|
|
3
|
+
<div v-if="hasLeading" class="cpBasket__trip">
|
|
4
|
+
<slot name="leading" />
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<cp-transition-expand>
|
|
8
|
+
<div v-if="hasSections">
|
|
9
|
+
<div class="cpBasket__details">
|
|
10
|
+
<div v-for="category in visibleCategories" :key="category.id" class="cpBasket__category">
|
|
11
|
+
<div class="cpBasket__categoryTitle">
|
|
12
|
+
<p>{{ category.label }}</p>
|
|
13
|
+
<p>{{ category.subtotal }}</p>
|
|
14
|
+
</div>
|
|
15
|
+
<div v-if="category.hasGroups" class="cpBasket__groups">
|
|
16
|
+
<cp-transition-list-items>
|
|
17
|
+
<div v-for="group in category.groups" :key="group.id" class="cpBasket__group">
|
|
18
|
+
<p v-if="group.label" class="cpBasket__route">{{ group.label }}</p>
|
|
19
|
+
<cp-transition-list-items>
|
|
20
|
+
<div v-for="row in group.rows" :key="row.key" :class="group.rowClass">
|
|
21
|
+
<span>{{ row.label }}</span>
|
|
22
|
+
<span>{{ row.value }}</span>
|
|
23
|
+
</div>
|
|
24
|
+
</cp-transition-list-items>
|
|
25
|
+
</div>
|
|
26
|
+
</cp-transition-list-items>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</cp-transition-expand>
|
|
32
|
+
|
|
33
|
+
<div class="cpBasket__total">
|
|
34
|
+
<p>{{ totalLabel }}</p>
|
|
35
|
+
<p class="cpBasket__totalValue">{{ total }}</p>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div v-if="hasTrailing" class="cpBasket__trailing">
|
|
39
|
+
<slot name="trailing" />
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<script setup lang="ts">
|
|
45
|
+
import { computed, useSlots } from 'vue'
|
|
46
|
+
|
|
47
|
+
import type { CpBasketCategory, CpBasketRow } from '@/constants/CpBasketTypes'
|
|
48
|
+
|
|
49
|
+
import CpTransitionExpand from '@/components/CpTransitionExpand.vue'
|
|
50
|
+
import CpTransitionListItems from '@/components/CpTransitionListItems.vue'
|
|
51
|
+
|
|
52
|
+
interface Props {
|
|
53
|
+
hideDetailsOnCategories?: string[]
|
|
54
|
+
rows: CpBasketRow[]
|
|
55
|
+
total: string
|
|
56
|
+
totalLabel: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ResolvedRow extends CpBasketRow {
|
|
60
|
+
key: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ResolvedGroup {
|
|
64
|
+
id: string
|
|
65
|
+
label: string | null
|
|
66
|
+
rowClass: string
|
|
67
|
+
rows: ResolvedRow[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface ResolvedCategory {
|
|
71
|
+
groups: ResolvedGroup[]
|
|
72
|
+
hasGroups: boolean
|
|
73
|
+
id: string
|
|
74
|
+
label: string
|
|
75
|
+
subtotal: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
79
|
+
hideDetailsOnCategories: () => [],
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const UNGROUPED_ID = '__cpBasketUngrouped__'
|
|
83
|
+
|
|
84
|
+
const slots = useSlots()
|
|
85
|
+
|
|
86
|
+
const hasLeading = computed(() => !!slots.leading)
|
|
87
|
+
|
|
88
|
+
const hasTrailing = computed(() => !!slots.trailing)
|
|
89
|
+
|
|
90
|
+
const hideDetailsIds = computed(() => new Set(props.hideDetailsOnCategories))
|
|
91
|
+
|
|
92
|
+
const dedupeById = <T extends { id: string }>(items: T[]): T[] =>
|
|
93
|
+
items.filter((item, index) => items.findIndex((other) => other.id === item.id) === index)
|
|
94
|
+
|
|
95
|
+
const orderedCategories = computed<CpBasketCategory[]>(() => dedupeById(props.rows.map((row) => row.category)))
|
|
96
|
+
|
|
97
|
+
const withKeys = (rows: CpBasketRow[], categoryId: string): ResolvedRow[] =>
|
|
98
|
+
rows.map((row) => ({ ...row, key: `${categoryId}-${row.group?.id ?? 'ungrouped'}-${row.label}` }))
|
|
99
|
+
|
|
100
|
+
const buildResolvedGroup = (id: string, label: string | null, rows: CpBasketRow[], categoryId: string): ResolvedGroup => ({
|
|
101
|
+
id,
|
|
102
|
+
label,
|
|
103
|
+
rowClass: label ? 'cpBasket__row cpBasket__row--isIndented' : 'cpBasket__row',
|
|
104
|
+
rows: withKeys(rows, categoryId),
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const buildResolvedCategory = (category: CpBasketCategory, categoryRows: CpBasketRow[]): ResolvedCategory => {
|
|
108
|
+
if (hideDetailsIds.value.has(category.id)) {
|
|
109
|
+
return { groups: [], hasGroups: false, id: category.id, label: category.label, subtotal: category.subtotal }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const groupsInCategory = categoryRows.flatMap((row) => (row.group ? [row.group] : []))
|
|
113
|
+
const uniqueGroups = dedupeById(groupsInCategory)
|
|
114
|
+
const labelledGroups = uniqueGroups.map((group) =>
|
|
115
|
+
buildResolvedGroup(
|
|
116
|
+
group.id,
|
|
117
|
+
group.label,
|
|
118
|
+
categoryRows.filter((row) => row.group?.id === group.id),
|
|
119
|
+
category.id,
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const ungroupedRows = categoryRows.filter((row) => row.group == null)
|
|
124
|
+
const ungroupedGroup: ResolvedGroup[] =
|
|
125
|
+
ungroupedRows.length === 0 ? [] : [buildResolvedGroup(UNGROUPED_ID, null, ungroupedRows, category.id)]
|
|
126
|
+
|
|
127
|
+
const groups = [...labelledGroups, ...ungroupedGroup]
|
|
128
|
+
|
|
129
|
+
return { groups, hasGroups: groups.length > 0, id: category.id, label: category.label, subtotal: category.subtotal }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const visibleCategories = computed<ResolvedCategory[]>(() =>
|
|
133
|
+
orderedCategories.value.map((category) =>
|
|
134
|
+
buildResolvedCategory(
|
|
135
|
+
category,
|
|
136
|
+
props.rows.filter((row) => row.category.id === category.id),
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
const hasSections = computed(() => visibleCategories.value.length > 0)
|
|
142
|
+
</script>
|
|
143
|
+
|
|
144
|
+
<style lang="scss">
|
|
145
|
+
.cpBasket {
|
|
146
|
+
display: flex;
|
|
147
|
+
flex-direction: column;
|
|
148
|
+
padding: var(--cp-spacing-sm);
|
|
149
|
+
border-radius: var(--cp-radius-xl);
|
|
150
|
+
background: var(--cp-background-tertiary);
|
|
151
|
+
gap: var(--cp-spacing-sm);
|
|
152
|
+
|
|
153
|
+
&__trip {
|
|
154
|
+
display: flex;
|
|
155
|
+
flex-direction: column;
|
|
156
|
+
padding: var(--cp-spacing-lg);
|
|
157
|
+
gap: var(--cp-spacing-md);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
&__leg {
|
|
161
|
+
display: flex;
|
|
162
|
+
flex-direction: column;
|
|
163
|
+
gap: var(--cp-spacing-sm);
|
|
164
|
+
|
|
165
|
+
& + & {
|
|
166
|
+
padding-top: var(--cp-spacing-md);
|
|
167
|
+
border-top: fn.px-to-rem(1) solid var(--cp-border-soft);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
&__legDate {
|
|
172
|
+
color: var(--cp-text-secondary);
|
|
173
|
+
font-size: var(--cp-text-size-xs);
|
|
174
|
+
font-weight: 600;
|
|
175
|
+
line-height: var(--cp-text-xs-line-height);
|
|
176
|
+
text-transform: uppercase;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
&__details {
|
|
180
|
+
display: flex;
|
|
181
|
+
overflow: hidden;
|
|
182
|
+
flex-direction: column;
|
|
183
|
+
padding: var(--cp-spacing-md) var(--cp-spacing-lg);
|
|
184
|
+
border-radius: var(--cp-radius-lg);
|
|
185
|
+
background: var(--cp-background-secondary);
|
|
186
|
+
gap: var(--cp-spacing-sm-md);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
&__category {
|
|
190
|
+
display: flex;
|
|
191
|
+
flex-direction: column;
|
|
192
|
+
|
|
193
|
+
& + & {
|
|
194
|
+
padding-top: var(--cp-spacing-sm-md);
|
|
195
|
+
border-top: fn.px-to-rem(1) solid var(--cp-border-soft);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
&__categoryTitle {
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
justify-content: space-between;
|
|
203
|
+
color: var(--cp-text-primary);
|
|
204
|
+
font-size: var(--cp-text-size-md);
|
|
205
|
+
font-weight: 600;
|
|
206
|
+
line-height: var(--cp-text-md-line-height);
|
|
207
|
+
padding-block: var(--cp-spacing-sm);
|
|
208
|
+
|
|
209
|
+
& > :last-child {
|
|
210
|
+
color: var(--cp-text-secondary);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
&__groups {
|
|
215
|
+
display: flex;
|
|
216
|
+
flex-direction: column;
|
|
217
|
+
gap: var(--cp-spacing-md);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
&__group {
|
|
221
|
+
position: relative;
|
|
222
|
+
display: flex;
|
|
223
|
+
flex-direction: column;
|
|
224
|
+
gap: var(--cp-spacing-sm);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
&__route {
|
|
228
|
+
color: var(--cp-text-secondary);
|
|
229
|
+
font-size: var(--cp-text-size-sm);
|
|
230
|
+
font-weight: 500;
|
|
231
|
+
line-height: var(--cp-text-sm-line-height);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
&__row {
|
|
235
|
+
display: flex;
|
|
236
|
+
width: 100%;
|
|
237
|
+
justify-content: space-between;
|
|
238
|
+
color: var(--cp-text-secondary);
|
|
239
|
+
font-size: var(--cp-text-size-sm);
|
|
240
|
+
gap: var(--cp-spacing-md);
|
|
241
|
+
line-height: var(--cp-text-sm-line-height);
|
|
242
|
+
|
|
243
|
+
&--isIndented {
|
|
244
|
+
padding-left: var(--cp-spacing-md);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
&__total {
|
|
249
|
+
display: flex;
|
|
250
|
+
align-items: flex-end;
|
|
251
|
+
justify-content: space-between;
|
|
252
|
+
padding: var(--cp-spacing-lg);
|
|
253
|
+
color: var(--cp-text-primary);
|
|
254
|
+
font-size: var(--cp-text-size-lg);
|
|
255
|
+
font-weight: 600;
|
|
256
|
+
line-height: var(--cp-text-lg-line-height);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
&__totalValue {
|
|
260
|
+
color: var(--cp-text-accent-primary);
|
|
261
|
+
font-variant-numeric: tabular-nums;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
&__trailing {
|
|
265
|
+
display: flex;
|
|
266
|
+
flex-direction: column;
|
|
267
|
+
align-items: center;
|
|
268
|
+
gap: var(--cp-spacing-sm);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
</style>
|
|
@@ -80,6 +80,12 @@ const increment = () => {
|
|
|
80
80
|
@include mx.square-sizing(28);
|
|
81
81
|
@extend %u-focus-outline;
|
|
82
82
|
|
|
83
|
+
.cpIcon {
|
|
84
|
+
flex-shrink: 0;
|
|
85
|
+
|
|
86
|
+
@include mx.square-sizing(16);
|
|
87
|
+
}
|
|
88
|
+
|
|
83
89
|
&:disabled {
|
|
84
90
|
border-color: transparent;
|
|
85
91
|
background: var(--cp-background-secondary);
|
package/src/components/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import CpAncillaryCategory from './CpAncillaryCategory.vue'
|
|
|
24
24
|
import CpAncillaryFlightSelection from './CpAncillaryFlightSelection.vue'
|
|
25
25
|
import CpAncillaryItem from './CpAncillaryItem.vue'
|
|
26
26
|
import CpBadge from './CpBadge.vue'
|
|
27
|
+
import CpBasket from './CpBasket.vue'
|
|
27
28
|
import CpBottomSheet from './CpBottomSheet.vue'
|
|
28
29
|
import CpButton from './CpButton.vue'
|
|
29
30
|
import CpButtonGroup from './CpButtonGroup.vue'
|
|
@@ -123,6 +124,7 @@ const Components = {
|
|
|
123
124
|
CpAncillaryCategory,
|
|
124
125
|
CpAncillaryFlightSelection,
|
|
125
126
|
CpAncillaryItem,
|
|
127
|
+
CpBasket,
|
|
126
128
|
CpLoader,
|
|
127
129
|
CpInput,
|
|
128
130
|
CpText,
|
|
@@ -198,6 +200,7 @@ const Pimp: Plugin = {
|
|
|
198
200
|
}
|
|
199
201
|
|
|
200
202
|
export type { CpAncillaryFlightSelectionFlight } from '@/constants/CpAncillaryFlightSelectionTypes'
|
|
203
|
+
export type { CpBasketCategory, CpBasketGroup, CpBasketRow } from '@/constants/CpBasketTypes'
|
|
201
204
|
export type { CpStepperStep } from '@/constants/CpStepperTypes'
|
|
202
205
|
export type {
|
|
203
206
|
CpSeatMapConfig,
|
|
@@ -243,6 +246,7 @@ export {
|
|
|
243
246
|
CpAncillaryFlightSelection,
|
|
244
247
|
CpAncillaryItem,
|
|
245
248
|
CpBadge,
|
|
249
|
+
CpBasket,
|
|
246
250
|
CpBottomSheet,
|
|
247
251
|
CpButton,
|
|
248
252
|
CpButtonGroup,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface CpBasketCategory {
|
|
2
|
+
id: string
|
|
3
|
+
label: string
|
|
4
|
+
subtotal: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface CpBasketGroup {
|
|
8
|
+
id: string
|
|
9
|
+
label: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CpBasketRow {
|
|
13
|
+
category: CpBasketCategory
|
|
14
|
+
group?: CpBasketGroup | null
|
|
15
|
+
label: string
|
|
16
|
+
value: string
|
|
17
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import type { Args, Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
import type { CpBasketRow } from '@/constants/CpBasketTypes'
|
|
5
|
+
|
|
6
|
+
import CpBasket from '@/components/CpBasket.vue'
|
|
7
|
+
import CpButton from '@/components/CpButton.vue'
|
|
8
|
+
import CpIcon from '@/components/CpIcon.vue'
|
|
9
|
+
import CpTrip from '@/components/CpTrip.vue'
|
|
10
|
+
|
|
11
|
+
const outboundTrip = {
|
|
12
|
+
arrivalDate: {
|
|
13
|
+
formatted: 'Tue, 10 Jun 2026',
|
|
14
|
+
raw: '2026-06-10',
|
|
15
|
+
},
|
|
16
|
+
arrivalTime: '10:20',
|
|
17
|
+
labels: {
|
|
18
|
+
nonStop: 'Direct',
|
|
19
|
+
},
|
|
20
|
+
dayCountAfterDeparture: 0,
|
|
21
|
+
departureDate: {
|
|
22
|
+
formatted: 'Tue, 10 Jun 2026',
|
|
23
|
+
raw: '2026-06-10',
|
|
24
|
+
},
|
|
25
|
+
departureTime: '09:00',
|
|
26
|
+
destination: {
|
|
27
|
+
iataCode: 'ORY',
|
|
28
|
+
name: 'Paris Orly Airport',
|
|
29
|
+
},
|
|
30
|
+
flightDuration: {
|
|
31
|
+
formatted: '1h 20m',
|
|
32
|
+
raw: 'PT1H20M',
|
|
33
|
+
},
|
|
34
|
+
airlines: [
|
|
35
|
+
{
|
|
36
|
+
iataCode: 'AF',
|
|
37
|
+
name: 'Air France',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
origin: {
|
|
41
|
+
iataCode: 'CDG',
|
|
42
|
+
name: 'Paris Charles de Gaulle Airport',
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const returnTrip = {
|
|
47
|
+
arrivalDate: {
|
|
48
|
+
formatted: 'Fri, 13 Jun 2026',
|
|
49
|
+
raw: '2026-06-13',
|
|
50
|
+
},
|
|
51
|
+
arrivalTime: '19:50',
|
|
52
|
+
labels: {
|
|
53
|
+
nonStop: 'Direct',
|
|
54
|
+
},
|
|
55
|
+
dayCountAfterDeparture: 0,
|
|
56
|
+
departureDate: {
|
|
57
|
+
formatted: 'Fri, 13 Jun 2026',
|
|
58
|
+
raw: '2026-06-13',
|
|
59
|
+
},
|
|
60
|
+
departureTime: '18:30',
|
|
61
|
+
destination: {
|
|
62
|
+
iataCode: 'CDG',
|
|
63
|
+
name: 'Paris Charles de Gaulle Airport',
|
|
64
|
+
},
|
|
65
|
+
flightDuration: {
|
|
66
|
+
formatted: '1h 20m',
|
|
67
|
+
raw: 'PT1H20M',
|
|
68
|
+
},
|
|
69
|
+
airlines: [
|
|
70
|
+
{
|
|
71
|
+
iataCode: 'AF',
|
|
72
|
+
name: 'Air France',
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
origin: {
|
|
76
|
+
iataCode: 'ORY',
|
|
77
|
+
name: 'Paris Orly Airport',
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const seatsCategory = { id: 'seats', label: 'Seats', subtotal: '24,00 €' }
|
|
82
|
+
const baggageCategory = { id: 'baggage', label: 'Baggage', subtotal: '20,00 €' }
|
|
83
|
+
const extrasCategory = { id: 'extras', label: 'Extras', subtotal: '3,00 €' }
|
|
84
|
+
|
|
85
|
+
const outboundGroup = { id: 'CDG-ORY', label: 'CDG → ORY' }
|
|
86
|
+
const returnGroup = { id: 'ORY-CDG', label: 'ORY → CDG' }
|
|
87
|
+
|
|
88
|
+
const rows: CpBasketRow[] = [
|
|
89
|
+
{ label: 'Seat 12A', value: '12,00 €', category: seatsCategory, group: outboundGroup },
|
|
90
|
+
{ label: 'Seat 14C', value: '12,00 €', category: seatsCategory, group: returnGroup },
|
|
91
|
+
{ label: '2 × Cabin baggage 10kg', value: '20,00 €', category: baggageCategory, group: outboundGroup },
|
|
92
|
+
{ label: 'Carbon offset', value: '3,00 €', category: extrasCategory },
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
const fakeRowPool: CpBasketRow[] = [
|
|
96
|
+
{ label: 'Extra bag 20kg', value: '15,00 €', category: baggageCategory, group: outboundGroup },
|
|
97
|
+
{ label: 'Seat 15B', value: '10,00 €', category: seatsCategory, group: returnGroup },
|
|
98
|
+
{ label: 'Meal', value: '9,00 €', category: extrasCategory },
|
|
99
|
+
{ label: 'Priority boarding', value: '8,00 €', category: extrasCategory, group: null },
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
const flightsCategory = { id: 'flights', label: 'Flights', subtotal: '159,80 €' }
|
|
103
|
+
const hiddenSeatsCategory = { id: 'seats', label: 'Seats', subtotal: '99,60 €' }
|
|
104
|
+
const tripExtrasCategory = { id: 'tripExtras', label: 'Trip extras', subtotal: '269,40 €' }
|
|
105
|
+
const taxesCategory = { id: 'taxes', label: 'Taxes & Fees', subtotal: '14,30 €' }
|
|
106
|
+
|
|
107
|
+
const hiddenDetailsRows: CpBasketRow[] = [
|
|
108
|
+
{ label: 'CDG → MRS', value: '79,90 €', category: flightsCategory, group: outboundGroup },
|
|
109
|
+
{ label: 'MRS → CDG', value: '79,90 €', category: flightsCategory, group: returnGroup },
|
|
110
|
+
{ label: 'Extra legroom', value: '99,60 €', category: hiddenSeatsCategory },
|
|
111
|
+
{ label: 'Priority boarding', value: '269,40 €', category: tripExtrasCategory },
|
|
112
|
+
{ label: 'VAT', value: '14,30 €', category: taxesCategory },
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
const meta = {
|
|
116
|
+
title: 'Business/CpBasket',
|
|
117
|
+
component: CpBasket,
|
|
118
|
+
argTypes: {
|
|
119
|
+
hideDetailsOnCategories: {
|
|
120
|
+
control: 'object',
|
|
121
|
+
description: 'Category ids rendered as a title-only line, with no groups or rows',
|
|
122
|
+
table: { defaultValue: { summary: '[]' } },
|
|
123
|
+
},
|
|
124
|
+
rows: {
|
|
125
|
+
control: 'object',
|
|
126
|
+
description: 'Flat list of basket rows, each carrying its own category and group',
|
|
127
|
+
},
|
|
128
|
+
total: {
|
|
129
|
+
control: 'text',
|
|
130
|
+
description: 'Preformatted total amount',
|
|
131
|
+
},
|
|
132
|
+
totalLabel: {
|
|
133
|
+
control: 'text',
|
|
134
|
+
description: 'Label shown next to the total, translated by the consumer',
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
} satisfies Meta<typeof CpBasket>
|
|
138
|
+
|
|
139
|
+
export default meta
|
|
140
|
+
|
|
141
|
+
type Story = StoryObj<typeof meta>
|
|
142
|
+
|
|
143
|
+
const defaultRender = (args: Args) => ({
|
|
144
|
+
components: { CpBasket, CpTrip },
|
|
145
|
+
setup() {
|
|
146
|
+
return { args, outboundTrip, returnTrip }
|
|
147
|
+
},
|
|
148
|
+
template: `
|
|
149
|
+
<CpBasket v-bind="args">
|
|
150
|
+
<template #leading>
|
|
151
|
+
<div class="cpBasket__leg">
|
|
152
|
+
<p class="cpBasket__legDate">{{ outboundTrip.departureDate.formatted }}</p>
|
|
153
|
+
<CpTrip hide-airlines :trip="outboundTrip" />
|
|
154
|
+
</div>
|
|
155
|
+
<div class="cpBasket__leg">
|
|
156
|
+
<p class="cpBasket__legDate">{{ returnTrip.departureDate.formatted }}</p>
|
|
157
|
+
<CpTrip hide-airlines :trip="returnTrip" />
|
|
158
|
+
</div>
|
|
159
|
+
</template>
|
|
160
|
+
</CpBasket>
|
|
161
|
+
`,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const defaultStoryRender = (args: Args) => ({
|
|
165
|
+
components: { CpBasket, CpButton, CpTrip },
|
|
166
|
+
setup() {
|
|
167
|
+
const storyRows = ref<CpBasketRow[]>([...rows])
|
|
168
|
+
const fakeRowCount = ref(0)
|
|
169
|
+
|
|
170
|
+
const addFakeRow = () => {
|
|
171
|
+
const pick = fakeRowPool[Math.floor(Math.random() * fakeRowPool.length)]
|
|
172
|
+
fakeRowCount.value += 1
|
|
173
|
+
storyRows.value = [...storyRows.value, { ...pick, label: `${pick.label} #${fakeRowCount.value}` }]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { addFakeRow, args, outboundTrip, returnTrip, storyRows }
|
|
177
|
+
},
|
|
178
|
+
template: `
|
|
179
|
+
<div style="display: flex; flex-direction: column; gap: 16px; max-width: 415px;">
|
|
180
|
+
<CpButton @click="addFakeRow">Add fake row</CpButton>
|
|
181
|
+
<CpBasket v-bind="args" :rows="storyRows">
|
|
182
|
+
<template #leading>
|
|
183
|
+
<div class="cpBasket__leg">
|
|
184
|
+
<p class="cpBasket__legDate">{{ outboundTrip.departureDate.formatted }}</p>
|
|
185
|
+
<CpTrip hide-airlines :trip="outboundTrip" />
|
|
186
|
+
</div>
|
|
187
|
+
<div class="cpBasket__leg">
|
|
188
|
+
<p class="cpBasket__legDate">{{ returnTrip.departureDate.formatted }}</p>
|
|
189
|
+
<CpTrip hide-airlines :trip="returnTrip" />
|
|
190
|
+
</div>
|
|
191
|
+
</template>
|
|
192
|
+
</CpBasket>
|
|
193
|
+
</div>
|
|
194
|
+
`,
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Three categories (seats, baggage, extras) with rows grouped per route in the
|
|
199
|
+
* `#leading` slot rendering the two `cp-trip` legs. The button above the
|
|
200
|
+
* basket appends one random fake row per click (sometimes ungrouped) to
|
|
201
|
+
* eyeball the `cp-transition-list-items` insert animation.
|
|
202
|
+
*/
|
|
203
|
+
export const Default: Story = {
|
|
204
|
+
args: {
|
|
205
|
+
rows,
|
|
206
|
+
total: '47,00 €',
|
|
207
|
+
totalLabel: 'Total',
|
|
208
|
+
},
|
|
209
|
+
render: defaultStoryRender,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* No rows: only the `#leading` slot and the total render, the details block
|
|
214
|
+
* collapses via `cp-transition-expand`.
|
|
215
|
+
*/
|
|
216
|
+
export const EmptyRows: Story = {
|
|
217
|
+
args: {
|
|
218
|
+
rows: [],
|
|
219
|
+
total: '0,00 €',
|
|
220
|
+
totalLabel: 'Total',
|
|
221
|
+
},
|
|
222
|
+
render: defaultRender,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* One category mixing a labelled group with ungrouped rows whose `group` is
|
|
227
|
+
* either omitted or explicitly `null` — both render the same way: no label,
|
|
228
|
+
* no indentation, after the labelled group.
|
|
229
|
+
*/
|
|
230
|
+
export const GroupedAndUngroupedRows: Story = {
|
|
231
|
+
args: {
|
|
232
|
+
rows: [
|
|
233
|
+
{
|
|
234
|
+
label: 'Seat 12A',
|
|
235
|
+
value: '12,00 €',
|
|
236
|
+
category: { id: 'seats', label: 'Seats', subtotal: '32,00 €' },
|
|
237
|
+
group: outboundGroup,
|
|
238
|
+
},
|
|
239
|
+
{ label: 'Seat 14C', value: '12,00 €', category: { id: 'seats', label: 'Seats', subtotal: '32,00 €' } },
|
|
240
|
+
{
|
|
241
|
+
label: 'Priority boarding',
|
|
242
|
+
value: '8,00 €',
|
|
243
|
+
category: { id: 'seats', label: 'Seats', subtotal: '32,00 €' },
|
|
244
|
+
group: null,
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
total: '32,00 €',
|
|
248
|
+
totalLabel: 'Total',
|
|
249
|
+
},
|
|
250
|
+
render: defaultRender,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* No `#leading` slot content: the leading container is not rendered at all.
|
|
255
|
+
*/
|
|
256
|
+
export const NoLeadingSlot: Story = {
|
|
257
|
+
args: {
|
|
258
|
+
rows,
|
|
259
|
+
total: '47,00 €',
|
|
260
|
+
totalLabel: 'Total',
|
|
261
|
+
},
|
|
262
|
+
render: (args: Args) => ({
|
|
263
|
+
components: { CpBasket },
|
|
264
|
+
setup() {
|
|
265
|
+
return { args }
|
|
266
|
+
},
|
|
267
|
+
template: `<CpBasket v-bind="args" />`,
|
|
268
|
+
}),
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* `hideDetailsOnCategories` collapses "Seats", "Trip extras" and
|
|
273
|
+
* "Taxes & Fees" to their title line while "Flights" keeps its grouped rows,
|
|
274
|
+
* matching the Figma payment-summary reference.
|
|
275
|
+
*/
|
|
276
|
+
export const HiddenDetails: Story = {
|
|
277
|
+
args: {
|
|
278
|
+
hideDetailsOnCategories: ['seats', 'tripExtras', 'taxes'],
|
|
279
|
+
rows: hiddenDetailsRows,
|
|
280
|
+
total: '543,10 €',
|
|
281
|
+
totalLabel: 'Total',
|
|
282
|
+
},
|
|
283
|
+
render: defaultRender,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* `#trailing` slot rendered after the total, with a `var(--cp-spacing-sm)`
|
|
288
|
+
* gap only appearing because the slot is provided.
|
|
289
|
+
*/
|
|
290
|
+
export const TrailingSlot: Story = {
|
|
291
|
+
args: {
|
|
292
|
+
rows,
|
|
293
|
+
total: '47,00 €',
|
|
294
|
+
totalLabel: 'Total',
|
|
295
|
+
},
|
|
296
|
+
render: (args: Args) => ({
|
|
297
|
+
components: { CpBasket, CpButton, CpIcon, CpTrip },
|
|
298
|
+
setup() {
|
|
299
|
+
return { args, outboundTrip, returnTrip }
|
|
300
|
+
},
|
|
301
|
+
template: `
|
|
302
|
+
<CpBasket v-bind="args">
|
|
303
|
+
<template #leading>
|
|
304
|
+
<div class="cpBasket__leg">
|
|
305
|
+
<p class="cpBasket__legDate">{{ outboundTrip.departureDate.formatted }}</p>
|
|
306
|
+
<CpTrip hide-airlines :trip="outboundTrip" />
|
|
307
|
+
</div>
|
|
308
|
+
<div class="cpBasket__leg">
|
|
309
|
+
<p class="cpBasket__legDate">{{ returnTrip.departureDate.formatted }}</p>
|
|
310
|
+
<CpTrip hide-airlines :trip="returnTrip" />
|
|
311
|
+
</div>
|
|
312
|
+
</template>
|
|
313
|
+
<template #trailing>
|
|
314
|
+
<CpButton appearance="secondary" color="accent" size="lg" style="width: 100%;">
|
|
315
|
+
<template #leading-icon><CpIcon type="download" /></template>
|
|
316
|
+
Download your invoice (PDF)
|
|
317
|
+
</CpButton>
|
|
318
|
+
<p style="color: var(--cp-text-secondary); font-size: 14px;">Paid on Jul 25, 2026 · card •• 4242</p>
|
|
319
|
+
</template>
|
|
320
|
+
</CpBasket>
|
|
321
|
+
`,
|
|
322
|
+
}),
|
|
323
|
+
}
|