@citizenplane/pimp 18.23.3 → 18.23.5
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 +13 -2
- package/dist/components/CpBasket.vue.d.ts.map +1 -1
- package/dist/pimp.es.js +9283 -9280
- package/dist/pimp.umd.js +60 -60
- package/dist/style.css +1 -1
- package/package.json +3 -1
- package/src/components/CpBasket.spec.ts +90 -0
- package/src/components/CpBasket.vue +10 -6
- package/src/components/CpQuantityCounter.vue +1 -1
- package/src/components/__snapshots__/CpBasket.spec.ts.snap +36 -0
- package/src/stories/CpBasket.stories.ts +110 -1
- package/tsconfig.build.json +10 -1
- package/vitest.config.js +32 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@citizenplane/pimp",
|
|
3
|
-
"version": "18.23.
|
|
3
|
+
"version": "18.23.5",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "storybook dev -p 8081",
|
|
6
6
|
"build-storybook": "storybook build --output-dir ./docs",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"lint:style:fix": "stylelint \"**/*.{css,scss,vue}\" --fix",
|
|
12
12
|
"format": "biome format --write .",
|
|
13
13
|
"types": "vue-tsc --noEmit",
|
|
14
|
+
"test": "vitest run",
|
|
14
15
|
"commitlint": "commitlint --edit",
|
|
15
16
|
"prepare": "husky"
|
|
16
17
|
},
|
|
@@ -74,6 +75,7 @@
|
|
|
74
75
|
"@vitest/coverage-v8": "4.1.9",
|
|
75
76
|
"@vue/test-utils": "2.4.11",
|
|
76
77
|
"commitlint-config-gitmoji": "2.3.1",
|
|
78
|
+
"happy-dom": "20.14.5",
|
|
77
79
|
"husky": "9.1.7",
|
|
78
80
|
"lint-staged": "17.2.0",
|
|
79
81
|
"playwright": "1.62.0",
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { defineComponent } from 'vue'
|
|
4
|
+
|
|
5
|
+
import type { CpBasketRow } from '@/constants/CpBasketTypes'
|
|
6
|
+
|
|
7
|
+
import CpBasket from '@/components/CpBasket.vue'
|
|
8
|
+
|
|
9
|
+
const seatsCategory = { id: 'seats', label: 'Seats', subtotal: '24,00 €' }
|
|
10
|
+
const promoCategory = { id: 'promo', label: 'Promo code', subtotal: '-10,00 €' }
|
|
11
|
+
|
|
12
|
+
const outboundGroup = { id: 'CDG-ORY', label: 'CDG → ORY' }
|
|
13
|
+
|
|
14
|
+
const textOnlyRows: CpBasketRow[] = [
|
|
15
|
+
{ label: 'Seat 12A', value: '12,00 €', category: seatsCategory, group: outboundGroup },
|
|
16
|
+
{ label: 'Carbon offset', value: '3,00 €', category: seatsCategory },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
const promoRows: CpBasketRow[] = [{ label: 'Promo SUMMER25 applied', value: '-10,00 €', category: promoCategory }]
|
|
20
|
+
|
|
21
|
+
const defaultProps = { total: '47,00 €', totalLabel: 'Total' }
|
|
22
|
+
|
|
23
|
+
describe('CpBasket', () => {
|
|
24
|
+
it('renders text-only rows as two morphing spans', () => {
|
|
25
|
+
const wrapper = mount(CpBasket, { props: { ...defaultProps, rows: textOnlyRows } })
|
|
26
|
+
|
|
27
|
+
const renderedRows = wrapper.findAll('.cpBasket__row')
|
|
28
|
+
|
|
29
|
+
expect(renderedRows).toHaveLength(2)
|
|
30
|
+
expect(renderedRows[0].element.innerHTML).toBe('<span class="">Seat 12A</span><span class="">12,00 €</span>')
|
|
31
|
+
expect(renderedRows[1].element.innerHTML).toBe('<span class="">Carbon offset</span><span class="">3,00 €</span>')
|
|
32
|
+
expect(wrapper.html()).toMatchSnapshot()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('keeps the row markup untouched when only one cell slot is provided', () => {
|
|
36
|
+
const wrapper = mount(CpBasket, {
|
|
37
|
+
props: { ...defaultProps, rows: promoRows },
|
|
38
|
+
slots: { rowValue: '<span class="promoValue">{{ params.row.value }}</span>' },
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const renderedRow = wrapper.get('.cpBasket__row')
|
|
42
|
+
|
|
43
|
+
expect(renderedRow.element.children).toHaveLength(2)
|
|
44
|
+
expect(renderedRow.element.children[0].outerHTML).toBe('<span class="">Promo SUMMER25 applied</span>')
|
|
45
|
+
expect(renderedRow.get('.promoValue').text()).toBe('-10,00 €')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('reaches the consumer handler when a button in the label slot is clicked', async () => {
|
|
49
|
+
const removePromoCode = vi.fn()
|
|
50
|
+
|
|
51
|
+
const host = defineComponent({
|
|
52
|
+
components: { CpBasket },
|
|
53
|
+
setup: () => ({ defaultProps, promoRows, removePromoCode }),
|
|
54
|
+
template: `
|
|
55
|
+
<CpBasket v-bind="defaultProps" :rows="promoRows">
|
|
56
|
+
<template #rowLabel="{ row }">
|
|
57
|
+
<span>{{ row.label }} <button type="button" @click="removePromoCode">Remove</button></span>
|
|
58
|
+
</template>
|
|
59
|
+
</CpBasket>
|
|
60
|
+
`,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const wrapper = mount(host)
|
|
64
|
+
|
|
65
|
+
const removeButton = wrapper.get('.cpBasket__row button')
|
|
66
|
+
|
|
67
|
+
expect(removeButton.element.tagName).toBe('BUTTON')
|
|
68
|
+
expect(removeButton.element.closest('.cpBasket__row')).toBe(wrapper.get('.cpBasket__row').element)
|
|
69
|
+
|
|
70
|
+
await removeButton.trigger('click')
|
|
71
|
+
|
|
72
|
+
expect(removePromoCode).toHaveBeenCalledTimes(1)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('right-aligns the value cell filled by the rowValue slot', () => {
|
|
76
|
+
const wrapper = mount(CpBasket, {
|
|
77
|
+
props: { ...defaultProps, rows: promoRows },
|
|
78
|
+
slots: { rowValue: '<span class="promoValue">{{ params.row.value }}</span>' },
|
|
79
|
+
attachTo: document.body,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const valueCell = wrapper.get('.promoValue').element
|
|
83
|
+
const valueCellStyle = window.getComputedStyle(valueCell)
|
|
84
|
+
|
|
85
|
+
expect(valueCellStyle.textAlign).toBe('right')
|
|
86
|
+
expect(valueCellStyle.fontVariantNumeric).toBe('tabular-nums')
|
|
87
|
+
|
|
88
|
+
wrapper.unmount()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -16,12 +16,10 @@
|
|
|
16
16
|
<cp-transition-list-items>
|
|
17
17
|
<div v-for="group in category.groups" :key="group.id" class="cpBasket__group">
|
|
18
18
|
<p v-if="group.label" class="cpBasket__route">{{ group.label }}</p>
|
|
19
|
-
<
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
</div>
|
|
24
|
-
</cp-transition-list-items>
|
|
19
|
+
<div v-for="row in group.rows" :key="row.key" :class="group.rowClass">
|
|
20
|
+
<slot name="rowLabel" :row="row"><cp-text-morph :text="row.label" as="span" /></slot>
|
|
21
|
+
<slot name="rowValue" :row="row"><cp-text-morph :text="row.value" as="span" /></slot>
|
|
22
|
+
</div>
|
|
25
23
|
</div>
|
|
26
24
|
</cp-transition-list-items>
|
|
27
25
|
</div>
|
|
@@ -46,6 +44,7 @@ import { computed, useSlots } from 'vue'
|
|
|
46
44
|
|
|
47
45
|
import type { CpBasketCategory, CpBasketRow } from '@/constants/CpBasketTypes'
|
|
48
46
|
|
|
47
|
+
import CpTextMorph from '@/components/CpTextMorph.vue'
|
|
49
48
|
import CpTransitionExpand from '@/components/CpTransitionExpand.vue'
|
|
50
49
|
import CpTransitionListItems from '@/components/CpTransitionListItems.vue'
|
|
51
50
|
|
|
@@ -243,6 +242,11 @@ const hasSections = computed(() => visibleCategories.value.length > 0)
|
|
|
243
242
|
&--isIndented {
|
|
244
243
|
padding-left: var(--cp-spacing-md);
|
|
245
244
|
}
|
|
245
|
+
|
|
246
|
+
& > :last-child {
|
|
247
|
+
font-variant-numeric: tabular-nums;
|
|
248
|
+
text-align: right;
|
|
249
|
+
}
|
|
246
250
|
}
|
|
247
251
|
|
|
248
252
|
&__total {
|
|
@@ -64,7 +64,7 @@ const increment = () => {
|
|
|
64
64
|
align-items: center;
|
|
65
65
|
padding: var(--cp-spacing-xs);
|
|
66
66
|
border-radius: var(--cp-radius-md-lg);
|
|
67
|
-
background: var(--cp-background-
|
|
67
|
+
background: var(--cp-background-secondary);
|
|
68
68
|
gap: var(--cp-spacing-sm);
|
|
69
69
|
|
|
70
70
|
&__button {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`CpBasket > renders text-only rows as two morphing spans 1`] = `
|
|
4
|
+
"<div class="cpBasket">
|
|
5
|
+
<!--v-if-->
|
|
6
|
+
<transition-stub data-v-19d824c7="" name="expand" appear="false" persisted="false" css="true">
|
|
7
|
+
<div>
|
|
8
|
+
<div class="cpBasket__details">
|
|
9
|
+
<div class="cpBasket__category">
|
|
10
|
+
<div class="cpBasket__categoryTitle">
|
|
11
|
+
<p>Seats</p>
|
|
12
|
+
<p>24,00 €</p>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="cpBasket__groups">
|
|
15
|
+
<transition-group-stub name="list-items" appear="false" persisted="false" css="true">
|
|
16
|
+
<div class="cpBasket__group">
|
|
17
|
+
<p class="cpBasket__route">CDG → ORY</p>
|
|
18
|
+
<div class="cpBasket__row cpBasket__row--isIndented"><span class="">Seat 12A</span><span class="">12,00 €</span></div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="cpBasket__group">
|
|
21
|
+
<!--v-if-->
|
|
22
|
+
<div class="cpBasket__row"><span class="">Carbon offset</span><span class="">3,00 €</span></div>
|
|
23
|
+
</div>
|
|
24
|
+
</transition-group-stub>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</transition-stub>
|
|
30
|
+
<div class="cpBasket__total">
|
|
31
|
+
<p>Total</p>
|
|
32
|
+
<p class="cpBasket__totalValue">47,00 €</p>
|
|
33
|
+
</div>
|
|
34
|
+
<!--v-if-->
|
|
35
|
+
</div>"
|
|
36
|
+
`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Args, Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
-
import { ref } from 'vue'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
3
|
|
|
4
4
|
import type { CpBasketRow } from '@/constants/CpBasketTypes'
|
|
5
5
|
|
|
@@ -99,6 +99,36 @@ const fakeRowPool: CpBasketRow[] = [
|
|
|
99
99
|
{ label: 'Priority boarding', value: '8,00 €', category: extrasCategory, group: null },
|
|
100
100
|
]
|
|
101
101
|
|
|
102
|
+
const morphBaseRows: CpBasketRow[] = [
|
|
103
|
+
{ label: 'Seat 12A', value: '12,00 €', category: { ...seatsCategory, subtotal: '12,00 €' }, group: outboundGroup },
|
|
104
|
+
{
|
|
105
|
+
label: 'Cabin baggage 10kg',
|
|
106
|
+
value: '20,00 €',
|
|
107
|
+
category: { ...baggageCategory, subtotal: '20,00 €' },
|
|
108
|
+
group: outboundGroup,
|
|
109
|
+
},
|
|
110
|
+
{ label: 'Carbon offset', value: '3,00 €', category: { ...extrasCategory, subtotal: '3,00 €' } },
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
const morphDoubledRows: CpBasketRow[] = [
|
|
114
|
+
{ label: 'Seat 12A', value: '24,00 €', category: { ...seatsCategory, subtotal: '24,00 €' }, group: outboundGroup },
|
|
115
|
+
{
|
|
116
|
+
label: 'Cabin baggage 10kg',
|
|
117
|
+
value: '40,00 €',
|
|
118
|
+
category: { ...baggageCategory, subtotal: '40,00 €' },
|
|
119
|
+
group: outboundGroup,
|
|
120
|
+
},
|
|
121
|
+
{ label: 'Carbon offset', value: '6,00 €', category: { ...extrasCategory, subtotal: '6,00 €' } },
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
const promoCategory = { id: 'promo', label: 'Promo code', subtotal: '-10,00 €' }
|
|
125
|
+
|
|
126
|
+
const promoRows: CpBasketRow[] = [
|
|
127
|
+
{ label: 'Seat 12A', value: '12,00 €', category: seatsCategory, group: outboundGroup },
|
|
128
|
+
{ label: '2 × Cabin baggage 10kg', value: '20,00 €', category: baggageCategory, group: outboundGroup },
|
|
129
|
+
{ label: 'Promo SUMMER25 applied', value: '-10,00 €', category: promoCategory },
|
|
130
|
+
]
|
|
131
|
+
|
|
102
132
|
const flightsCategory = { id: 'flights', label: 'Flights', subtotal: '159,80 €' }
|
|
103
133
|
const hiddenSeatsCategory = { id: 'seats', label: 'Seats', subtotal: '99,60 €' }
|
|
104
134
|
const tripExtrasCategory = { id: 'tripExtras', label: 'Trip extras', subtotal: '269,40 €' }
|
|
@@ -321,3 +351,82 @@ export const TrailingSlot: Story = {
|
|
|
321
351
|
`,
|
|
322
352
|
}),
|
|
323
353
|
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The rows keep their identity while their amounts change: clicking "Double
|
|
357
|
+
* the quantities" only swaps `row.value`, so each value morphs in place
|
|
358
|
+
* through `cp-text-morph` instead of the row re-entering the list.
|
|
359
|
+
*/
|
|
360
|
+
export const MorphingRowValues: Story = {
|
|
361
|
+
args: {
|
|
362
|
+
rows: morphBaseRows,
|
|
363
|
+
total: '35,00 €',
|
|
364
|
+
totalLabel: 'Total',
|
|
365
|
+
},
|
|
366
|
+
render: (args: Args) => ({
|
|
367
|
+
components: { CpBasket, CpButton },
|
|
368
|
+
setup() {
|
|
369
|
+
const isDoubled = ref(false)
|
|
370
|
+
|
|
371
|
+
const storyRows = computed<CpBasketRow[]>(() => (isDoubled.value ? morphDoubledRows : morphBaseRows))
|
|
372
|
+
const storyTotal = computed<string>(() => (isDoubled.value ? '70,00 €' : '35,00 €'))
|
|
373
|
+
|
|
374
|
+
const toggleQuantities = () => {
|
|
375
|
+
isDoubled.value = !isDoubled.value
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return { args, storyRows, storyTotal, toggleQuantities }
|
|
379
|
+
},
|
|
380
|
+
template: `
|
|
381
|
+
<div style="display: flex; flex-direction: column; gap: 16px; max-width: 415px;">
|
|
382
|
+
<CpButton @click="toggleQuantities">Double the quantities</CpButton>
|
|
383
|
+
<CpBasket v-bind="args" :rows="storyRows" :total="storyTotal" />
|
|
384
|
+
</div>
|
|
385
|
+
`,
|
|
386
|
+
}),
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* `#rowLabel` and `#rowValue` let the consumer own one cell of a row while the
|
|
391
|
+
* rest of the basket keeps its default text rendering. Here the promo row
|
|
392
|
+
* carries a real `cp-button` next to its label and a success-toned negative
|
|
393
|
+
* amount, and clicking "Remove" drops the row from the list.
|
|
394
|
+
*/
|
|
395
|
+
export const PromoCodeRow: Story = {
|
|
396
|
+
args: {
|
|
397
|
+
rows: promoRows,
|
|
398
|
+
total: '22,00 €',
|
|
399
|
+
totalLabel: 'Total',
|
|
400
|
+
},
|
|
401
|
+
render: (args: Args) => ({
|
|
402
|
+
components: { CpBasket, CpButton },
|
|
403
|
+
setup() {
|
|
404
|
+
const storyRows = ref<CpBasketRow[]>([...promoRows])
|
|
405
|
+
|
|
406
|
+
const removePromoCode = () => {
|
|
407
|
+
storyRows.value = storyRows.value.filter((row) => row.category.id !== promoCategory.id)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return { args, promoCategoryId: promoCategory.id, removePromoCode, storyRows }
|
|
411
|
+
},
|
|
412
|
+
template: `
|
|
413
|
+
<div style="max-width: 415px;">
|
|
414
|
+
<CpBasket v-bind="args" :rows="storyRows">
|
|
415
|
+
<template #rowLabel="{ row }">
|
|
416
|
+
<span v-if="row.category.id === promoCategoryId">
|
|
417
|
+
{{ row.label }}
|
|
418
|
+
<CpButton appearance="tertiary" color="accent" size="2xs" @click="removePromoCode">Remove</CpButton>
|
|
419
|
+
</span>
|
|
420
|
+
<span v-else>{{ row.label }}</span>
|
|
421
|
+
</template>
|
|
422
|
+
<template #rowValue="{ row }">
|
|
423
|
+
<span v-if="row.category.id === promoCategoryId" style="color: var(--cp-text-success-primary);">
|
|
424
|
+
{{ row.value }}
|
|
425
|
+
</span>
|
|
426
|
+
<span v-else>{{ row.value }}</span>
|
|
427
|
+
</template>
|
|
428
|
+
</CpBasket>
|
|
429
|
+
</div>
|
|
430
|
+
`,
|
|
431
|
+
}),
|
|
432
|
+
}
|
package/tsconfig.build.json
CHANGED
|
@@ -10,5 +10,14 @@
|
|
|
10
10
|
"skipLibCheck": true
|
|
11
11
|
},
|
|
12
12
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/**/*.d.ts"],
|
|
13
|
-
"exclude": [
|
|
13
|
+
"exclude": [
|
|
14
|
+
"node_modules",
|
|
15
|
+
"dist",
|
|
16
|
+
"src/stories",
|
|
17
|
+
"**/*.spec.ts",
|
|
18
|
+
"**/*.stories.ts",
|
|
19
|
+
"**/*.stories.vue",
|
|
20
|
+
"**/*.stories.tsx",
|
|
21
|
+
"tests"
|
|
22
|
+
]
|
|
14
23
|
}
|
package/vitest.config.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import vue from '@vitejs/plugin-vue'
|
|
4
|
+
import { defineConfig } from 'vitest/config'
|
|
5
|
+
|
|
6
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
plugins: [vue()],
|
|
10
|
+
resolve: {
|
|
11
|
+
alias: {
|
|
12
|
+
'@': path.resolve(dirname, 'src'),
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
css: {
|
|
16
|
+
preprocessorOptions: {
|
|
17
|
+
scss: {
|
|
18
|
+
additionalData: `
|
|
19
|
+
@use "sass:math";
|
|
20
|
+
@use '@/assets/styles/helpers/functions' as fn;
|
|
21
|
+
@use '@/assets/styles/helpers/mixins' as mx;
|
|
22
|
+
@use '@/assets/styles/utilities';`,
|
|
23
|
+
api: 'modern-compiler',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
test: {
|
|
28
|
+
css: true,
|
|
29
|
+
environment: 'happy-dom',
|
|
30
|
+
include: ['src/**/*.spec.ts'],
|
|
31
|
+
},
|
|
32
|
+
})
|