@citizenplane/pimp 18.11.0 → 18.12.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/CpSegmentedControl.vue.d.ts +29 -0
- package/dist/components/CpSegmentedControl.vue.d.ts.map +1 -0
- package/dist/components/CpSegmentedControlItem.vue.d.ts +29 -0
- package/dist/components/CpSegmentedControlItem.vue.d.ts.map +1 -0
- package/dist/components/CpTabs.vue.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/pimp.es.js +4405 -4271
- package/dist/pimp.umd.js +7 -7
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/assets/css/shadows.css +10 -0
- package/src/assets/css/tokens.css +8 -0
- package/src/components/CpSegmentedControl.vue +210 -0
- package/src/components/CpSegmentedControlItem.vue +248 -0
- package/src/components/CpTabs.vue +7 -12
- package/src/components/index.ts +3 -0
- package/src/stories/CpSeatMap.stories.ts +56 -36
- package/src/stories/CpSegmentedControl.stories.ts +261 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
2
|
import { action } from 'storybook/actions'
|
|
3
3
|
import { computed, ref, watch } from 'vue'
|
|
4
|
+
import type { ComponentProps } from 'vue-component-type-helpers'
|
|
4
5
|
|
|
5
6
|
import type { CpSeatMapSeatSelection } from '@/constants/seatMap/CpSeatMapSeatSelection'
|
|
6
7
|
import type { Seat } from '@/constants/seatMap/Seat'
|
|
@@ -10,30 +11,57 @@ import type { SeatRow } from '@/constants/seatMap/SeatRow'
|
|
|
10
11
|
import type { SeatRowFacilityFlags } from '@/constants/seatMap/SeatRowFacilityFlags'
|
|
11
12
|
|
|
12
13
|
import CpSeatMap from '@/components/CpSeatMap.vue'
|
|
13
|
-
import CpSelect from '@/components/CpSelect.vue'
|
|
14
14
|
|
|
15
15
|
import { IataTravelerTypes } from '@/constants/seatMap/IataTravelerTypes'
|
|
16
16
|
import { SeatStatuses } from '@/constants/seatMap/SeatStatuses'
|
|
17
17
|
import { displaySeat } from '@/helpers/seatMap'
|
|
18
18
|
|
|
19
|
+
const AIRCRAFT_LABELS = {
|
|
20
|
+
'atr-72': 'ATR 72 — 2+2',
|
|
21
|
+
a320: 'A320 — 3+3',
|
|
22
|
+
b777: 'B777 — 3+4+3',
|
|
23
|
+
} as const
|
|
24
|
+
|
|
25
|
+
type AircraftId = keyof typeof AIRCRAFT_LABELS
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `aircraft` is a story-only knob: the component has no such prop, the render
|
|
29
|
+
* function turns it into the matching `seatMap`, which is therefore hidden.
|
|
30
|
+
*/
|
|
31
|
+
type CpSeatMapArgs = Omit<ComponentProps<typeof CpSeatMap>, 'seatMap'> & {
|
|
32
|
+
aircraft: AircraftId
|
|
33
|
+
seatMap?: SeatMap
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
const meta = {
|
|
20
37
|
title: 'Business/CpSeatMap',
|
|
21
38
|
component: CpSeatMap,
|
|
22
39
|
parameters: {
|
|
23
40
|
layout: 'padded',
|
|
24
41
|
},
|
|
42
|
+
args: {
|
|
43
|
+
aircraft: 'a320',
|
|
44
|
+
},
|
|
45
|
+
argTypes: {
|
|
46
|
+
aircraft: {
|
|
47
|
+
control: { type: 'select', labels: AIRCRAFT_LABELS },
|
|
48
|
+
options: Object.keys(AIRCRAFT_LABELS),
|
|
49
|
+
description: 'Story-only: picks the seat map handed to the component.',
|
|
50
|
+
},
|
|
51
|
+
seatMap: { control: false, table: { disable: true } },
|
|
52
|
+
},
|
|
25
53
|
// The plane wings sit 180px outside the cabin, so the story needs side room.
|
|
26
54
|
decorators: [
|
|
27
55
|
() => ({
|
|
28
56
|
template:
|
|
29
|
-
'<div style="margin: 0 auto; padding: 40px; border-radius: 8px; background-color: #fff;"><story/></div>',
|
|
57
|
+
'<div style="display: flex; justify-content: center;margin: 0 auto; padding: 40px; border-radius: 8px; background-color: #fff;"><story/></div>',
|
|
30
58
|
}),
|
|
31
59
|
],
|
|
32
|
-
} satisfies Meta<
|
|
60
|
+
} satisfies Meta<CpSeatMapArgs>
|
|
33
61
|
|
|
34
62
|
export default meta
|
|
35
63
|
|
|
36
|
-
type Story = StoryObj<
|
|
64
|
+
type Story = StoryObj<CpSeatMapArgs>
|
|
37
65
|
|
|
38
66
|
const EXTRA_LEGROOM = {
|
|
39
67
|
amount: 12,
|
|
@@ -244,18 +272,12 @@ const b777 = makeSeatMap('B777', [
|
|
|
244
272
|
},
|
|
245
273
|
])
|
|
246
274
|
|
|
247
|
-
const AIRCRAFT: Record<
|
|
275
|
+
const AIRCRAFT: Record<AircraftId, SeatMap> = {
|
|
248
276
|
'atr-72': atr72,
|
|
249
277
|
a320,
|
|
250
278
|
b777,
|
|
251
279
|
}
|
|
252
280
|
|
|
253
|
-
const AIRCRAFT_OPTIONS = [
|
|
254
|
-
{ label: 'ATR 72 — 2+2', value: 'atr-72' },
|
|
255
|
-
{ label: 'A320 — 3+3', value: 'a320' },
|
|
256
|
-
{ label: 'B777 — 3+4+3', value: 'b777' },
|
|
257
|
-
]
|
|
258
|
-
|
|
259
281
|
const selection = (
|
|
260
282
|
row: number,
|
|
261
283
|
column: string,
|
|
@@ -265,27 +287,36 @@ const selection = (
|
|
|
265
287
|
})
|
|
266
288
|
|
|
267
289
|
/**
|
|
268
|
-
*
|
|
290
|
+
* The `aircraft` control picks the map, so one aircraft can be compared against
|
|
291
|
+
* another from the controls panel.
|
|
269
292
|
*
|
|
270
293
|
* `isInteractive` wires the click back into `selections`, which is what a consumer
|
|
271
294
|
* has to do: the map only reports the seat, it never remembers it.
|
|
272
295
|
*/
|
|
273
|
-
const
|
|
296
|
+
const withAircraftControl =
|
|
274
297
|
(isInteractive = false) =>
|
|
275
|
-
(args:
|
|
276
|
-
components: { CpSeatMap
|
|
298
|
+
(args: CpSeatMapArgs) => ({
|
|
299
|
+
components: { CpSeatMap },
|
|
277
300
|
setup() {
|
|
278
|
-
const
|
|
279
|
-
const seatMap = computed(() => AIRCRAFT[aircraft.value] as SeatMap)
|
|
301
|
+
const seatMap = computed(() => AIRCRAFT[args.aircraft])
|
|
280
302
|
|
|
281
303
|
const ownSelections = ref<Record<string, CpSeatMapSeatSelection>>({})
|
|
282
304
|
const selections = computed(() => (isInteractive ? ownSelections.value : args.selections))
|
|
283
305
|
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
306
|
+
// `aircraft` isn't a component prop, so it's kept off `v-bind`.
|
|
307
|
+
const componentArgs = computed(() => {
|
|
308
|
+
const { aircraft, seatMap: _, ...rest } = args
|
|
309
|
+
return rest
|
|
287
310
|
})
|
|
288
311
|
|
|
312
|
+
// A seat key from one aircraft rarely exists on the next one.
|
|
313
|
+
watch(
|
|
314
|
+
() => args.aircraft,
|
|
315
|
+
() => {
|
|
316
|
+
ownSelections.value = {}
|
|
317
|
+
},
|
|
318
|
+
)
|
|
319
|
+
|
|
289
320
|
const handleSelect = (seat: Seat, rowNumber: number) => {
|
|
290
321
|
action('select')(seat, rowNumber)
|
|
291
322
|
if (!isInteractive) return
|
|
@@ -295,16 +326,10 @@ const withAircraftPicker =
|
|
|
295
326
|
ownSelections.value = key in ownSelections.value ? {} : { [key]: { initials: 'RN' } }
|
|
296
327
|
}
|
|
297
328
|
|
|
298
|
-
return {
|
|
329
|
+
return { componentArgs, seatMap, selections, handleSelect }
|
|
299
330
|
},
|
|
300
|
-
// The plane nose bleeds upwards out of the cabin, so the picker is lifted above it.
|
|
301
331
|
template: `
|
|
302
|
-
<
|
|
303
|
-
<div style="position: absolute; top: 24px; left: 24px; z-index: 1; max-width: 280px; background-color: #fff;">
|
|
304
|
-
<cp-select v-model="aircraft" hide-default-value label="Aircraft" :options="aircraftOptions" />
|
|
305
|
-
</div>
|
|
306
|
-
<cp-seat-map v-bind="args" :seat-map="seatMap" :selections="selections" @select="handleSelect" />
|
|
307
|
-
</div>
|
|
332
|
+
<cp-seat-map v-bind="componentArgs" :seat-map="seatMap" :selections="selections" @select="handleSelect" />
|
|
308
333
|
`,
|
|
309
334
|
})
|
|
310
335
|
|
|
@@ -314,10 +339,7 @@ const withAircraftPicker =
|
|
|
314
339
|
* blocked and paid-for seats read from the map.
|
|
315
340
|
*/
|
|
316
341
|
export const Default: Story = {
|
|
317
|
-
|
|
318
|
-
seatMap: a320,
|
|
319
|
-
},
|
|
320
|
-
render: withAircraftPicker(true),
|
|
342
|
+
render: withAircraftControl(true),
|
|
321
343
|
}
|
|
322
344
|
|
|
323
345
|
/**
|
|
@@ -326,14 +348,13 @@ export const Default: Story = {
|
|
|
326
348
|
*/
|
|
327
349
|
export const WithSelection: Story = {
|
|
328
350
|
args: {
|
|
329
|
-
seatMap: a320,
|
|
330
351
|
selections: {
|
|
331
352
|
...selection(7, 'B', { initials: 'JD' }),
|
|
332
353
|
...selection(8, 'C', { initials: 'AM' }),
|
|
333
354
|
},
|
|
334
355
|
onSelect: action('select'),
|
|
335
356
|
},
|
|
336
|
-
render:
|
|
357
|
+
render: withAircraftControl(),
|
|
337
358
|
}
|
|
338
359
|
|
|
339
360
|
/**
|
|
@@ -342,7 +363,6 @@ export const WithSelection: Story = {
|
|
|
342
363
|
*/
|
|
343
364
|
export const WithLinkedInfant: Story = {
|
|
344
365
|
args: {
|
|
345
|
-
seatMap: a320,
|
|
346
366
|
selections: {
|
|
347
367
|
...selection(7, 'B', {
|
|
348
368
|
initials: 'JD',
|
|
@@ -353,5 +373,5 @@ export const WithLinkedInfant: Story = {
|
|
|
353
373
|
},
|
|
354
374
|
onSelect: action('select'),
|
|
355
375
|
},
|
|
356
|
-
render:
|
|
376
|
+
render: withAircraftControl(),
|
|
357
377
|
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
import CpSegmentedControl from '@/components/CpSegmentedControl.vue'
|
|
5
|
+
|
|
6
|
+
import { docCellStyle, docLabelStyle, docRowColumnStyle } from '@/stories/documentationStyles'
|
|
7
|
+
|
|
8
|
+
const segmentedControlSizes = ['sm', 'md'] as const
|
|
9
|
+
|
|
10
|
+
const meta = {
|
|
11
|
+
title: 'Molecules/CpSegmentedControl',
|
|
12
|
+
component: CpSegmentedControl,
|
|
13
|
+
argTypes: {
|
|
14
|
+
modelValue: {
|
|
15
|
+
control: 'text',
|
|
16
|
+
description: 'The selected option value',
|
|
17
|
+
},
|
|
18
|
+
name: {
|
|
19
|
+
control: 'text',
|
|
20
|
+
description: 'Name attribute for the radio group',
|
|
21
|
+
},
|
|
22
|
+
options: {
|
|
23
|
+
control: 'object',
|
|
24
|
+
description: 'Array of segmented options',
|
|
25
|
+
},
|
|
26
|
+
size: {
|
|
27
|
+
control: 'select',
|
|
28
|
+
options: segmentedControlSizes,
|
|
29
|
+
description: 'The size of the segmented control',
|
|
30
|
+
},
|
|
31
|
+
isDisabled: {
|
|
32
|
+
control: 'boolean',
|
|
33
|
+
description: 'Whether the control is disabled',
|
|
34
|
+
},
|
|
35
|
+
isFullWidth: {
|
|
36
|
+
control: 'boolean',
|
|
37
|
+
description: 'Whether each option stretches to fill the available width',
|
|
38
|
+
},
|
|
39
|
+
isInvalid: {
|
|
40
|
+
control: 'boolean',
|
|
41
|
+
description: 'Whether the control is in an invalid state',
|
|
42
|
+
},
|
|
43
|
+
orientation: {
|
|
44
|
+
control: 'select',
|
|
45
|
+
options: ['horizontal', 'vertical'],
|
|
46
|
+
description: 'The orientation of the control',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
} satisfies Meta<typeof CpSegmentedControl>
|
|
50
|
+
|
|
51
|
+
export default meta
|
|
52
|
+
type Story = StoryObj<typeof meta>
|
|
53
|
+
|
|
54
|
+
const sampleOptions = [
|
|
55
|
+
{ value: 'option1', label: 'Option 1', leadingIcon: 'dashed-circle', trailingIcon: 'dashed-circle' },
|
|
56
|
+
{ value: 'option2', label: 'Option 2', leadingIcon: 'dashed-circle', trailingIcon: 'dashed-circle' },
|
|
57
|
+
{ value: 'option3', label: 'Option 3', leadingIcon: 'dashed-circle', trailingIcon: 'dashed-circle' },
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
const createRender = (label: string) => (args: { modelValue?: string; isFullWidth?: boolean }) => ({
|
|
61
|
+
components: { CpSegmentedControl },
|
|
62
|
+
setup() {
|
|
63
|
+
const value = ref(args.modelValue)
|
|
64
|
+
|
|
65
|
+
watch(
|
|
66
|
+
() => args.modelValue,
|
|
67
|
+
(modelValue) => {
|
|
68
|
+
value.value = modelValue
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const boundArgs = computed(() => {
|
|
73
|
+
const { modelValue: _modelValue, ...rest } = args
|
|
74
|
+
return rest
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const cellStyle = args.isFullWidth ? `${docCellStyle} width: 100%;` : docCellStyle
|
|
78
|
+
|
|
79
|
+
return { boundArgs, value, label, cellStyle, docLabelStyle, docRowColumnStyle }
|
|
80
|
+
},
|
|
81
|
+
template: `
|
|
82
|
+
<div :style="docRowColumnStyle">
|
|
83
|
+
<div :style="cellStyle">
|
|
84
|
+
<span :style="docLabelStyle">{{ label }}</span>
|
|
85
|
+
<CpSegmentedControl v-bind="boundArgs" v-model="value" />
|
|
86
|
+
<p :style="docLabelStyle">modelValue: <code>{{ value }}</code></p>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
`,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Default segmented control. Use the controls to experiment with each prop
|
|
94
|
+
* in isolation.
|
|
95
|
+
*/
|
|
96
|
+
export const Default: Story = {
|
|
97
|
+
args: {
|
|
98
|
+
modelValue: 'option1',
|
|
99
|
+
name: 'segmented-control-default',
|
|
100
|
+
options: sampleOptions,
|
|
101
|
+
size: 'md',
|
|
102
|
+
isFullWidth: false,
|
|
103
|
+
isInvalid: false,
|
|
104
|
+
},
|
|
105
|
+
render: createRender('Default'),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Options rendered with leading and trailing icons.
|
|
110
|
+
*/
|
|
111
|
+
export const WithIcons: Story = {
|
|
112
|
+
args: {
|
|
113
|
+
...Default.args,
|
|
114
|
+
name: 'segmented-control-icons',
|
|
115
|
+
options: [
|
|
116
|
+
{ value: 'list', label: 'List', leadingIcon: 'dashed-circle', trailingIcon: 'dashed-circle' },
|
|
117
|
+
{ value: 'grid', label: 'Grid', leadingIcon: 'dashed-circle', trailingIcon: 'dashed-circle' },
|
|
118
|
+
{ value: 'map', label: 'Map', leadingIcon: 'dashed-circle', trailingIcon: 'dashed-circle' },
|
|
119
|
+
],
|
|
120
|
+
modelValue: 'list',
|
|
121
|
+
},
|
|
122
|
+
render: createRender('With icons'),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Available sizes side by side — `sm` and `md`.
|
|
127
|
+
*/
|
|
128
|
+
export const Sizes: Story = {
|
|
129
|
+
args: {
|
|
130
|
+
...Default.args,
|
|
131
|
+
name: 'segmented-control-sizes',
|
|
132
|
+
},
|
|
133
|
+
parameters: { controls: { disable: true } },
|
|
134
|
+
render: () => ({
|
|
135
|
+
components: { CpSegmentedControl },
|
|
136
|
+
setup() {
|
|
137
|
+
const values = ref({
|
|
138
|
+
sm: 'option1',
|
|
139
|
+
md: 'option1',
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
return { values, sampleOptions, segmentedControlSizes, docCellStyle, docLabelStyle, docRowColumnStyle }
|
|
143
|
+
},
|
|
144
|
+
template: `
|
|
145
|
+
<div :style="docRowColumnStyle">
|
|
146
|
+
<div v-for="size in segmentedControlSizes" :key="size" :style="docCellStyle">
|
|
147
|
+
<span :style="docLabelStyle">{{ size }}</span>
|
|
148
|
+
<CpSegmentedControl
|
|
149
|
+
v-model="values[size]"
|
|
150
|
+
:name="'segmented-control-sizes-' + size"
|
|
151
|
+
:options="sampleOptions"
|
|
152
|
+
:size="size"
|
|
153
|
+
/>
|
|
154
|
+
<p :style="docLabelStyle">modelValue: <code>{{ values[size] }}</code></p>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
`,
|
|
158
|
+
}),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Stretch each option so the control fills its container with
|
|
163
|
+
* `isFullWidth`.
|
|
164
|
+
*/
|
|
165
|
+
export const FullWidth: Story = {
|
|
166
|
+
args: {
|
|
167
|
+
...Default.args,
|
|
168
|
+
name: 'segmented-control-full-width',
|
|
169
|
+
isFullWidth: true,
|
|
170
|
+
},
|
|
171
|
+
parameters: {
|
|
172
|
+
layout: 'padded',
|
|
173
|
+
},
|
|
174
|
+
decorators: [
|
|
175
|
+
() => ({
|
|
176
|
+
template: '<div style="width: 100%;"><story /></div>',
|
|
177
|
+
}),
|
|
178
|
+
],
|
|
179
|
+
render: (args) => ({
|
|
180
|
+
components: { CpSegmentedControl },
|
|
181
|
+
setup() {
|
|
182
|
+
const value = ref(args.modelValue)
|
|
183
|
+
|
|
184
|
+
watch(
|
|
185
|
+
() => args.modelValue,
|
|
186
|
+
(modelValue) => {
|
|
187
|
+
value.value = modelValue
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
const boundArgs = computed(() => {
|
|
192
|
+
const { modelValue: _modelValue, ...rest } = args
|
|
193
|
+
return rest
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const cellStyle = `${docCellStyle} width: 100%;`
|
|
197
|
+
|
|
198
|
+
return { boundArgs, value, cellStyle, docLabelStyle, docRowColumnStyle }
|
|
199
|
+
},
|
|
200
|
+
template: `
|
|
201
|
+
<div :style="docRowColumnStyle">
|
|
202
|
+
<div :style="cellStyle">
|
|
203
|
+
<span :style="docLabelStyle">Full width</span>
|
|
204
|
+
<CpSegmentedControl v-bind="boundArgs" v-model="value" />
|
|
205
|
+
<p :style="docLabelStyle">modelValue: <code>{{ value }}</code></p>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
`,
|
|
209
|
+
}),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Disable the whole control.
|
|
214
|
+
*/
|
|
215
|
+
export const WithDisabledOption: Story = {
|
|
216
|
+
args: {
|
|
217
|
+
...Default.args,
|
|
218
|
+
name: 'segmented-control-disabled',
|
|
219
|
+
isDisabled: true,
|
|
220
|
+
modelValue: undefined,
|
|
221
|
+
},
|
|
222
|
+
render: createRender('Disabled'),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* A disabled option can still be the current value. Use this pattern for
|
|
227
|
+
* read-only pre-selections.
|
|
228
|
+
*/
|
|
229
|
+
export const CheckedDisabled: Story = {
|
|
230
|
+
args: {
|
|
231
|
+
...Default.args,
|
|
232
|
+
name: 'segmented-control-checked-disabled',
|
|
233
|
+
isDisabled: true,
|
|
234
|
+
modelValue: 'option1',
|
|
235
|
+
},
|
|
236
|
+
render: createRender('Checked disabled'),
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Error state — highlights the control border when validation fails.
|
|
241
|
+
*/
|
|
242
|
+
export const Invalid: Story = {
|
|
243
|
+
args: {
|
|
244
|
+
...Default.args,
|
|
245
|
+
name: 'segmented-control-invalid',
|
|
246
|
+
isInvalid: true,
|
|
247
|
+
},
|
|
248
|
+
render: createRender('Invalid'),
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Vertical orientation
|
|
253
|
+
*/
|
|
254
|
+
export const Vertical: Story = {
|
|
255
|
+
args: {
|
|
256
|
+
...Default.args,
|
|
257
|
+
name: 'segmented-control-vertical',
|
|
258
|
+
orientation: 'vertical',
|
|
259
|
+
},
|
|
260
|
+
render: createRender('Vertical'),
|
|
261
|
+
}
|