@citizenplane/pimp 8.23.1 → 8.25.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.
Files changed (75) hide show
  1. package/dist/{IconAccompaniedMinorEach-BELsBJAA.js → IconAccompaniedMinorEach-DE6L9BRu.js} +1 -1
  2. package/dist/{IconAccompaniedMinorNone-B55gmNIs.js → IconAccompaniedMinorNone-CKwkfQWY.js} +1 -1
  3. package/dist/{IconAccompaniedMinorOne-CuZH-Uoj.js → IconAccompaniedMinorOne-CDw9ndVx.js} +1 -1
  4. package/dist/{IconAddReceipt-chTEnPzA.js → IconAddReceipt-C9EJNceo.js} +1 -1
  5. package/dist/{IconAirportTerminal-CLcg4rVw.js → IconAirportTerminal-B3zEFGce.js} +1 -1
  6. package/dist/{IconArrival-DrBGRnD6.js → IconArrival-aSQX9DwG.js} +1 -1
  7. package/dist/{IconBroadcast-a6rKq8z4.js → IconBroadcast-B3GbI4O2.js} +1 -1
  8. package/dist/{IconCabinBag-DSa8pX5b.js → IconCabinBag-1gMUAFiJ.js} +1 -1
  9. package/dist/{IconCheckedBaggage-2YqzTXY7.js → IconCheckedBaggage-Hgg2P-La.js} +1 -1
  10. package/dist/{IconCheckedBaggage20-CKhkn1aT.js → IconCheckedBaggage20-CuRwridK.js} +1 -1
  11. package/dist/{IconCheckedBaggage30-BpUlFJHv.js → IconCheckedBaggage30-Bu5mfGrU.js} +1 -1
  12. package/dist/{IconChild-DeB0UUM8.js → IconChild-DVzPdW18.js} +1 -1
  13. package/dist/{IconContact-DZp6vSjU.js → IconContact-vLLYEtHG.js} +1 -1
  14. package/dist/{IconDeparture-CQOGey2m.js → IconDeparture-jDaBhT9y.js} +1 -1
  15. package/dist/{IconDistribution-uXQmHNE5.js → IconDistribution-CXcKxl2c.js} +1 -1
  16. package/dist/{IconDistributionClosed-CNL9Ca1c.js → IconDistributionClosed-B5guexCV.js} +1 -1
  17. package/dist/{IconDistributionExclusivePair-gD30YmGV.js → IconDistributionExclusivePair-BweDwKkh.js} +1 -1
  18. package/dist/{IconDistributionSided-BoerBpdU.js → IconDistributionSided-DE4q1Sve.js} +1 -1
  19. package/dist/{IconDistributionSupplySided-C9M8k-ag.js → IconDistributionSupplySided-DPgYaUUP.js} +1 -1
  20. package/dist/{IconDynamicContent-DDXQuCXZ.js → IconDynamicContent-Dp7HDol5.js} +1 -1
  21. package/dist/{IconFares-D2J35zKW.js → IconFares-Bi5au_9V.js} +1 -1
  22. package/dist/{IconFaresOutlined-DvwGZ54W.js → IconFaresOutlined-CPgtsOsn.js} +1 -1
  23. package/dist/{IconFemale-8Feo-ZbL.js → IconFemale-Zx4vOVnU.js} +1 -1
  24. package/dist/{IconFindConversation-q68nIAf4.js → IconFindConversation--2ZwQxMw.js} +1 -1
  25. package/dist/{IconFire-C_zc55kT.js → IconFire-Bi__90Xq.js} +1 -1
  26. package/dist/{IconFlight-CYwhmf5A.js → IconFlight-DylPPVpp.js} +1 -1
  27. package/dist/{IconFlightReturn-Cdzy6WX8.js → IconFlightReturn-wcvC_zyk.js} +1 -1
  28. package/dist/{IconHandHeart-CC4bxoCM.js → IconHandHeart-ve95hsD0.js} +1 -1
  29. package/dist/{IconHistory-DoWlMxrP.js → IconHistory-BhVPp0cQ.js} +1 -1
  30. package/dist/{IconHourGlass-AHUuzz2r.js → IconHourGlass-DV8oqdyg.js} +1 -1
  31. package/dist/{IconIdCard-KSBgSPeJ.js → IconIdCard-DwzuzXN2.js} +1 -1
  32. package/dist/{IconInfant-BeaFioB2.js → IconInfant-DT1Nzl0x.js} +1 -1
  33. package/dist/{IconItinerary-CFj-6b5Q.js → IconItinerary-7Hb9iv3y.js} +1 -1
  34. package/dist/{IconLeave-CywEsLcw.js → IconLeave-dmrPJAf6.js} +1 -1
  35. package/dist/{IconMale-BEKZycPr.js → IconMale-DSjpnm3Q.js} +1 -1
  36. package/dist/{IconMultiSegments-Dn2EH-Md.js → IconMultiSegments-X6qnSwUS.js} +1 -1
  37. package/dist/{IconNoPassport-DrmjbxJD.js → IconNoPassport-C1HJBdD9.js} +1 -1
  38. package/dist/{IconNoRefund-ArIPLIjM.js → IconNoRefund-DiJN_FA8.js} +1 -1
  39. package/dist/{IconNotion-D7Dh8ajc.js → IconNotion-B-tSrJ4u.js} +1 -1
  40. package/dist/{IconOffline-BUJEYKmj.js → IconOffline-Ctj066_m.js} +1 -1
  41. package/dist/{IconOneWay-DKN_eckI.js → IconOneWay-BrcB89Tq.js} +1 -1
  42. package/dist/{IconPaid-BEw_R6DH.js → IconPaid-BzH5OLQb.js} +1 -1
  43. package/dist/{IconPassport-dH3GzIR9.js → IconPassport-8yGMToG1.js} +1 -1
  44. package/dist/{IconPayout-BOTWwPoR.js → IconPayout-CZXb-HTd.js} +1 -1
  45. package/dist/{IconReceipt-MoL2346d.js → IconReceipt-JA-M0U3n.js} +1 -1
  46. package/dist/{IconRecurrence-TvqFhOBh.js → IconRecurrence-mXMYoI0n.js} +1 -1
  47. package/dist/{IconRefund-CaTBeKJS.js → IconRefund-B2L5xKAF.js} +1 -1
  48. package/dist/{IconRoundTrip-DRsx92WJ.js → IconRoundTrip-COeSvwSW.js} +1 -1
  49. package/dist/{IconRouteNoStop-c_j4fcq7.js → IconRouteNoStop-BV2sWIsV.js} +1 -1
  50. package/dist/{IconRouteOneStop-enbNaG1p.js → IconRouteOneStop-BoxuuMO3.js} +1 -1
  51. package/dist/{IconScheduleChange-D3aH6iCp.js → IconScheduleChange-vtfvtcu7.js} +1 -1
  52. package/dist/{IconSeatEmpty-BNJbTjjk.js → IconSeatEmpty-CRKjSZ77.js} +1 -1
  53. package/dist/{IconSeatSold-DB8Y9qBr.js → IconSeatSold-DObYWLgC.js} +1 -1
  54. package/dist/{IconSeatTotal-BK4rmwfJ.js → IconSeatTotal-TfNbmsSp.js} +1 -1
  55. package/dist/{IconSemiMoon-CnO_tWYz.js → IconSemiMoon-2BPnLHjW.js} +1 -1
  56. package/dist/{IconTemplate-pqHRwkap.js → IconTemplate-D6ZRzCiS.js} +1 -1
  57. package/dist/{IconTicket-oPHpGyYt.js → IconTicket-Cx4JaCEo.js} +1 -1
  58. package/dist/{IconTimer-DCvoAyOE.js → IconTimer-CdfSGP1v.js} +1 -1
  59. package/dist/{IconTrafficControl-9QNWcYF5.js → IconTrafficControl-B83ebOK1.js} +1 -1
  60. package/dist/{IconWithPassport-BztuFvAs.js → IconWithPassport-BtjM5pk4.js} +1 -1
  61. package/dist/{index-CoogODBx.js → index-782z9Iy2.js} +10387 -4699
  62. package/dist/pimp.es.js +1 -1
  63. package/dist/pimp.umd.js +691 -3
  64. package/dist/style.css +1 -1
  65. package/package.json +2 -1
  66. package/src/assets/styles/helpers/_mixins.scss +37 -1
  67. package/src/assets/styles/utilities/_index.scss +14 -0
  68. package/src/components/core/BaseInputLabel.vue +6 -0
  69. package/src/components/core/BaseSelectClearButton.vue +36 -0
  70. package/src/components/index.ts +5 -0
  71. package/src/components/lists-and-table/CpTable.vue +89 -39
  72. package/src/components/selects/CpMultiselect.vue +426 -0
  73. package/src/helpers/object.js +9 -0
  74. package/src/stories/CpMultiselect.stories.ts +237 -0
  75. package/src/stories/CpTable.stories.ts +32 -22
@@ -0,0 +1,426 @@
1
+ <template>
2
+ <div class="cpMultiselect">
3
+ <base-input-label v-if="label" :required="required" class="cpMultiselect__label">
4
+ {{ label }}
5
+ </base-input-label>
6
+
7
+ <AutoComplete
8
+ ref="multiselect"
9
+ v-model="selectModel"
10
+ :suggestions="options"
11
+ :option-label="optionLabel"
12
+ :name="name"
13
+ force-selection
14
+ :data-key="trackBy"
15
+ :multiple="multiple"
16
+ input-class="cpMultiselect__input"
17
+ :invalid="isInvalid"
18
+ auto-option-focus
19
+ :placeholder="placeholder"
20
+ :disabled="disabled"
21
+ option-disabled="disabled"
22
+ :pt="passThroughConfig"
23
+ @update:model-value="handleUpdateModelValue"
24
+ @complete="handleSearch"
25
+ @keydown.esc.stop
26
+ >
27
+ <template #empty>
28
+ <slot name="empty">
29
+ <div class="cpMultiselect__empty">{{ emptyMessage }}</div>
30
+ </slot>
31
+ </template>
32
+
33
+ <template #chip="{ value, removeCallback }">
34
+ <slot name="selected-option" :option="value" :remove="removeCallback">
35
+ <cp-badge is-clearable size="sm" @on-clear="removeCallback()">
36
+ <template #leading-icon>
37
+ <slot name="selected-option-leading-icon" :option="value" />
38
+ </template>
39
+ {{ value.name }}
40
+ </cp-badge>
41
+ </slot>
42
+ </template>
43
+
44
+ <template #option="{ option }">
45
+ <slot name="option" :option="option" />
46
+ </template>
47
+
48
+ <template #dropdown>
49
+ <div v-if="displayPrefix" class="cpMultiselect__prefix">
50
+ <slot name="prefix" />
51
+ </div>
52
+
53
+ <cp-loader v-if="isLoading" class="cpMultiselect__loader" color="#B2B2BD" />
54
+ <button v-else :disabled="disabled" type="button" class="cpMultiselect__toggle" @click.stop="toggleDropdown">
55
+ <cp-icon type="chevron-down" class="cpMultiselect__dropdownIcon" :class="chevronDynamicClass" />
56
+ </button>
57
+
58
+ <base-select-clear-button v-if="displayClearButton" @click="handleClear" class="cpMultiselect__clear" />
59
+ </template>
60
+ </AutoComplete>
61
+
62
+ <transition-expand>
63
+ <p v-if="isInvalid" class="cpMultiselect__error">
64
+ {{ errorMessage }}
65
+ </p>
66
+ </transition-expand>
67
+ </div>
68
+ </template>
69
+
70
+ <script setup>
71
+ import { ref, computed, onMounted } from 'vue'
72
+ import AutoComplete from 'primevue/autocomplete'
73
+
74
+ import { absolutePosition, getOuterWidth } from '@primeuix/utils/dom'
75
+
76
+ import BaseInputLabel from '@/components/core/BaseInputLabel.vue'
77
+ import TransitionExpand from '@/components/helpers-utilities/TransitionExpand.vue'
78
+ import BaseSelectClearButton from '@/components/core/BaseSelectClearButton.vue'
79
+
80
+ import { isEmpty } from '@/helpers/object'
81
+
82
+ const props = defineProps({
83
+ label: {
84
+ type: String,
85
+ required: false,
86
+ default: '',
87
+ },
88
+ required: {
89
+ type: Boolean,
90
+ required: false,
91
+ default: false,
92
+ },
93
+ name: {
94
+ type: String,
95
+ required: false,
96
+ default: '',
97
+ },
98
+ placeholder: {
99
+ type: String,
100
+ required: false,
101
+ default: '',
102
+ },
103
+ isInvalid: {
104
+ type: Boolean,
105
+ required: false,
106
+ default: false,
107
+ },
108
+ isClearable: {
109
+ type: Boolean,
110
+ required: false,
111
+ default: false,
112
+ },
113
+ isLoading: {
114
+ type: Boolean,
115
+ required: false,
116
+ default: false,
117
+ },
118
+ disabled: {
119
+ type: Boolean,
120
+ required: false,
121
+ default: false,
122
+ },
123
+ multiple: {
124
+ type: Boolean,
125
+ required: false,
126
+ default: false,
127
+ },
128
+ options: {
129
+ type: Array,
130
+ required: false,
131
+ default: () => [],
132
+ },
133
+ optionLabel: {
134
+ type: String,
135
+ required: false,
136
+ default: 'name',
137
+ },
138
+ trackBy: {
139
+ type: String,
140
+ required: false,
141
+ default: 'id',
142
+ },
143
+ emptyMessage: {
144
+ type: String,
145
+ required: false,
146
+ default: 'No results found',
147
+ },
148
+ errorMessage: {
149
+ type: String,
150
+ required: false,
151
+ default: '',
152
+ },
153
+ modelValue: {
154
+ type: [Array, Object],
155
+ required: false,
156
+ },
157
+ })
158
+
159
+ const emit = defineEmits(['search', 'select', 'clear'])
160
+
161
+ const selectModel = computed({
162
+ get() {
163
+ return props.modelValue
164
+ },
165
+ set(value) {
166
+ if (typeof value === 'string') {
167
+ return
168
+ }
169
+
170
+ emit('update:modelValue', value)
171
+ },
172
+ })
173
+
174
+ const passThroughConfig = {
175
+ root: { class: 'cpMultiselect__select' },
176
+ inputmultiple: { class: 'cpMultiselect__tags' },
177
+ dropdown: { class: 'cpMultiselect__toggle' },
178
+ inputchip: { class: 'cpMultiselect__inputWrapper' },
179
+ overlay: { class: 'cpMultiselect__overlay' },
180
+ listcontainer: { class: 'cpMultiselect__listWrapper' },
181
+ list: { class: 'cpMultiselect__list' },
182
+ option: { class: 'cpMultiselect__option' },
183
+ loader: { class: 'cpMultiselect__hidden' },
184
+ }
185
+
186
+ const multiselect = ref(null)
187
+
188
+ const isDropdownOpen = computed(() => multiselect.value?.overlayVisible)
189
+
190
+ const chevronDynamicClass = computed(() => {
191
+ return {
192
+ 'cpMultiselect__dropdownIcon--isRotated': isDropdownOpen.value,
193
+ }
194
+ })
195
+
196
+ const displayPrefix = computed(() => {
197
+ if (!props.multiple) return true
198
+ return !selectModel.value?.length
199
+ })
200
+
201
+ const displayClearButton = computed(() => {
202
+ if (props.multiple) return false
203
+ return props.isClearable && !isEmpty(selectModel.value)
204
+ })
205
+
206
+ const handleSearch = (event) => emit('search', event.query)
207
+ const handleClear = () => (selectModel.value = null)
208
+
209
+ const toggleDropdown = () => {
210
+ if (isDropdownOpen.value) {
211
+ multiselect.value.hide()
212
+ } else {
213
+ multiselect.value.show()
214
+ }
215
+ }
216
+
217
+ const handleUpdateModelValue = (value) => {
218
+ // Autocomplete will set the model value to the query string if not blocked
219
+ if (!value || typeof value === 'string') {
220
+ return
221
+ }
222
+ }
223
+
224
+ const overrideAlignOverlay = () => (multiselect.value.alignOverlay = alignOverlay)
225
+
226
+ const alignOverlay = () => {
227
+ const target = multiselect.value.$el
228
+ if (!multiselect.value.overlay || !target) return
229
+
230
+ multiselect.value.overlay.style.width = `${getOuterWidth(target)}px`
231
+
232
+ absolutePosition(multiselect.value.overlay, target)
233
+ }
234
+
235
+ onMounted(() => overrideAlignOverlay())
236
+ </script>
237
+
238
+ <style lang="scss">
239
+ .cpMultiselect {
240
+ display: flex;
241
+ flex-direction: column;
242
+ gap: sp.$space;
243
+
244
+ &__label {
245
+ margin-bottom: 0;
246
+ }
247
+
248
+ &__prefix {
249
+ order: -1;
250
+
251
+ &:empty {
252
+ display: none;
253
+ }
254
+ }
255
+
256
+ &__select {
257
+ display: flex;
258
+ min-height: fn.px-to-rem(46);
259
+ align-items: center;
260
+ justify-content: space-between;
261
+ padding: fn.px-to-rem(8);
262
+ border: 1px solid colors.$border-color;
263
+ border-radius: fn.px-to-rem(10);
264
+ gap: sp.$space;
265
+
266
+ &:has(input:focus-visible) {
267
+ box-shadow: 0 0 0 fn.px-to-em(3) color.scale(colors.$primary-color, $lightness: 70%);
268
+ }
269
+
270
+ &:has(input:disabled) {
271
+ background-color: colors.$neutral-light-1;
272
+ cursor: not-allowed;
273
+ }
274
+
275
+ &:has([data-p='invalid']) {
276
+ border-color: colors.$error-color;
277
+
278
+ &:has(input:focus-visible) {
279
+ box-shadow: 0 0 0 fn.px-to-em(3) color.scale(colors.$error-color, $lightness: 70%);
280
+ }
281
+ }
282
+ }
283
+
284
+ &__tags {
285
+ display: flex;
286
+ flex: 1;
287
+ flex-wrap: wrap;
288
+ gap: sp.$space;
289
+ }
290
+
291
+ &__toggle {
292
+ @extend %u-focus-outline;
293
+
294
+ display: flex;
295
+ align-items: center;
296
+ justify-content: center;
297
+
298
+ &:disabled {
299
+ cursor: not-allowed;
300
+ }
301
+ }
302
+
303
+ &__inputWrapper {
304
+ display: flex;
305
+ flex: 1;
306
+ align-items: center;
307
+ }
308
+
309
+ &__input {
310
+ padding: 0;
311
+ flex: 1;
312
+ font-size: fn.px-to-rem(14);
313
+ line-height: fn.px-to-rem(24);
314
+ background-color: transparent;
315
+
316
+ &:disabled {
317
+ cursor: not-allowed;
318
+ }
319
+ }
320
+
321
+ &__input {
322
+ padding: 0;
323
+ flex: 1;
324
+ font-size: fn.px-to-rem(14);
325
+ line-height: fn.px-to-rem(24);
326
+ }
327
+
328
+ &__dropdownIcon {
329
+ @include mx.square-sizing(16);
330
+
331
+ transition: rotate 200ms ease;
332
+
333
+ &--isRotated {
334
+ rotate: 180deg;
335
+ }
336
+ }
337
+
338
+ &__loader {
339
+ @include mx.square-sizing(16);
340
+ }
341
+
342
+ &__overlay {
343
+ left: 0;
344
+ overflow: hidden;
345
+ background: colors.$neutral-light;
346
+ border-radius: fn.px-to-rem(8);
347
+ margin-block: sp.$space;
348
+ box-shadow:
349
+ 0 2px 4px 0 rgba(18, 18, 23, 0.04),
350
+ 0 5px 8px 0 rgba(18, 18, 23, 0.04),
351
+ 0 10px 18px 0 rgba(18, 18, 23, 0.03),
352
+ 0 24px 48px 0 rgba(18, 18, 23, 0.03),
353
+ 0 0 0 1px rgba(18, 18, 23, 0.1);
354
+ }
355
+
356
+ &__listWrapper {
357
+ overflow-y: auto;
358
+ }
359
+
360
+ &__hidden {
361
+ display: none;
362
+ }
363
+
364
+ &__list {
365
+ padding: sp.$space-sm;
366
+ }
367
+
368
+ &__option {
369
+ display: flex;
370
+ align-items: center;
371
+ padding: fn.px-to-rem(10);
372
+ border-radius: fn.px-to-rem(6);
373
+ gap: sp.$space-sm;
374
+ font-size: fn.px-to-rem(14);
375
+ line-height: fn.px-to-rem(18);
376
+
377
+ &:hover,
378
+ &[data-p-focused='true'],
379
+ &[data-p-selected='true'] {
380
+ background: colors.$neutral-dark-5;
381
+ color: colors.$neutral-dark;
382
+ }
383
+
384
+ &[data-p-selected='true'] {
385
+ border: 1px dashed colors.$border-color;
386
+ }
387
+
388
+ &[data-p-disabled='true'] {
389
+ opacity: 0.5;
390
+ cursor: not-allowed;
391
+ pointer-events: none;
392
+ }
393
+ }
394
+
395
+ &__empty {
396
+ padding: sp.$space;
397
+ font-size: fn.px-to-rem(14);
398
+ color: colors.$neutral-dark-1;
399
+ }
400
+
401
+ &__error {
402
+ color: colors.$error-color;
403
+ font-size: fn.px-to-rem(14);
404
+ font-weight: 500;
405
+ line-height: fn.px-to-rem(24);
406
+ }
407
+
408
+ @include mx.media-query-pointer-device-only {
409
+ &__clear {
410
+ display: none;
411
+ }
412
+ }
413
+ }
414
+
415
+ @include mx.media-query-pointer-device-only {
416
+ .cpMultiselect:has(.cpMultiselect__clear):is(:hover, :focus-within) {
417
+ .cpMultiselect__clear {
418
+ display: flex;
419
+ }
420
+
421
+ .cpMultiselect__toggle {
422
+ display: none;
423
+ }
424
+ }
425
+ }
426
+ </style>
@@ -0,0 +1,9 @@
1
+ export const isObject = (object) => {
2
+ return object != null && typeof object === 'object'
3
+ }
4
+
5
+ export const isEmpty = (obj) => {
6
+ if (!isObject(obj)) return true
7
+
8
+ return Reflect.ownKeys(obj).length === 0 && obj.constructor === Object
9
+ }
@@ -0,0 +1,237 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import { ref } from 'vue'
3
+
4
+ import CpMultiselect from '@/components/selects/CpMultiselect.vue'
5
+
6
+ const meta = {
7
+ title: 'CpMultiSelect',
8
+ component: CpMultiselect,
9
+ argTypes: {
10
+ label: {
11
+ control: 'text',
12
+ description: 'Label of the select',
13
+ },
14
+ required: {
15
+ control: 'boolean',
16
+ description: 'Whether the select is required',
17
+ },
18
+ name: {
19
+ control: 'text',
20
+ description: 'Name of the select',
21
+ },
22
+ placeholder: {
23
+ control: 'text',
24
+ description: 'Placeholder of the select',
25
+ },
26
+ isInvalid: {
27
+ control: 'boolean',
28
+ description: 'Whether the select is invalid',
29
+ },
30
+ multiple: {
31
+ control: 'boolean',
32
+ description: 'Whether the select should be multiple',
33
+ },
34
+ options: {
35
+ control: 'object',
36
+ description: 'Options of the select',
37
+ },
38
+ errorMessage: {
39
+ control: 'text',
40
+ description: 'Error message of the select',
41
+ },
42
+ disabled: {
43
+ control: 'boolean',
44
+ description: 'Whether the select is disabled',
45
+ },
46
+ isLoading: {
47
+ control: 'boolean',
48
+ description: 'Whether the select is loading',
49
+ },
50
+ emptyMessage: {
51
+ control: 'text',
52
+ description: 'Message displayed when no options are found',
53
+ },
54
+ trackBy: {
55
+ control: 'text',
56
+ description: 'Property to track the selected option',
57
+ },
58
+ },
59
+ } satisfies Meta<typeof CpMultiselect>
60
+
61
+ export default meta
62
+ type Story = StoryObj<typeof meta>
63
+
64
+ export const Single: Story = {
65
+ args: {
66
+ placeholder: 'Select a supplier',
67
+ multiple: false,
68
+ options: [
69
+ { id: 1, name: 'MGA AIRLINES' },
70
+ { id: 2, name: 'ZENITH - EUROATLANTIC AIRWAYS' },
71
+ { id: 3, name: 'ZENITH - SOUTHERN AIRWAYS EXPRESS MOKULELE AIRLINES AND SURF AIR MOBILITY' },
72
+ { id: 4, name: 'EDO TUNISAIR ITALY' },
73
+ { id: 5, name: 'LMX SUISSE SA' },
74
+ { id: 6, name: 'LMX VOYAGES SAS' },
75
+ { id: 7, name: 'FLY KHIVA TRAVEL' },
76
+ { id: 8, name: 'ZENITH - AIR CHATHAMS' },
77
+ { id: 9, name: 'CP EMPLOYEES' },
78
+ { id: 10, name: 'MONDIAL TOURISME' },
79
+ { id: 11, name: 'UNITRAVEL UTAZÁSI IRODA' },
80
+ ],
81
+ },
82
+ render: (args) => ({
83
+ components: { CpMultiselect },
84
+ setup() {
85
+ const searchQuery = ref('')
86
+ const isLoading = ref(false)
87
+
88
+ const originalOptions = ref(args.options)
89
+ const dynamicOptions = ref(originalOptions.value)
90
+
91
+ const handleSearch = async (query: string) => {
92
+ isLoading.value = true
93
+ searchQuery.value = query
94
+
95
+ dynamicOptions.value = originalOptions.value
96
+
97
+ await new Promise((resolve) => setTimeout(resolve, 500))
98
+
99
+ dynamicOptions.value = dynamicOptions.value.filter((option: { name: string }) => {
100
+ return option.name.toLowerCase().includes(searchQuery.value.toLowerCase())
101
+ })
102
+
103
+ isLoading.value = false
104
+ }
105
+
106
+ const selectedSupplier = ref(null)
107
+ return { args, selectedSupplier, dynamicOptions, handleSearch, isLoading }
108
+ },
109
+ template: `
110
+ <div style="padding: 20px;">
111
+ <CpMultiselect v-model="selectedSupplier" v-bind="args" :options="dynamicOptions" :is-loading="isLoading" @search="handleSearch">
112
+ <template #prefix>
113
+ <cp-partner-badge type="supplier" size="xs" />
114
+ </template>
115
+ <template #option="{ option }">
116
+ <div style="display: flex; align-items: center; gap: 8px;">
117
+ <cp-partner-badge type="supplier" size="xs" />
118
+ {{ option.name }}
119
+ </div>
120
+ </template>
121
+ </CpMultiselect>
122
+ </div>
123
+ `,
124
+ }),
125
+ }
126
+
127
+ export const Multiple: Story = {
128
+ args: {
129
+ required: false,
130
+ name: 'select',
131
+ placeholder: 'All airlines',
132
+ isInvalid: false,
133
+ disabled: false,
134
+ multiple: true,
135
+ emptyMessage: 'No airlines found',
136
+ options: [
137
+ { id: 1, name: 'United Airlines', iata_code: 'UA' },
138
+ { id: 2, name: 'Delta Airlines', iata_code: 'DL' },
139
+ { id: 3, name: 'American Airlines', iata_code: 'AA' },
140
+ { id: 4, name: 'Southwest Airlines', iata_code: 'WN' },
141
+ { id: 5, name: 'Alaska Airlines', iata_code: 'AS' },
142
+ { id: 6, name: 'JetBlue Airways', iata_code: 'B6' },
143
+ { id: 7, name: 'Spirit Airlines', iata_code: 'NK' },
144
+ { id: 8, name: 'Frontier Airlines', iata_code: 'F9' },
145
+ { id: 9, name: 'Hawaiian Airlines', iata_code: 'HA' },
146
+ { id: 10, name: 'SkyWest Airlines', iata_code: 'OO' },
147
+ { id: 11, name: 'Allegiant Air', iata_code: 'G4' },
148
+ { id: 12, name: 'Atlantic Southeast Airlines', iata_code: 'EV' },
149
+ { id: 13, name: 'American Eagle Airlines', iata_code: 'MQ' },
150
+ { id: 14, name: 'Alaska Airlines', iata_code: 'AS' },
151
+ { id: 15, name: 'Air France', iata_code: 'AF', disabled: true },
152
+ { id: 16, name: 'Air Canada', iata_code: 'AC' },
153
+ { id: 17, name: 'Air New Zealand', iata_code: 'NZ' },
154
+ { id: 18, name: 'Air China', iata_code: 'CA' },
155
+ { id: 19, name: 'Air India', iata_code: 'AI' },
156
+ { id: 20, name: 'Air Berlin', iata_code: 'AB' },
157
+ { id: 21, name: 'AirAsia', iata_code: 'AK' },
158
+ ],
159
+ },
160
+ render: (args) => ({
161
+ components: { CpMultiselect },
162
+ setup() {
163
+ const searchQuery = ref('')
164
+ const isLoading = ref(false)
165
+
166
+ const originalOptions = ref(args.options)
167
+ const dynamicOptions = ref(originalOptions.value)
168
+
169
+ const handleSearch = async (query: string) => {
170
+ isLoading.value = true
171
+ searchQuery.value = query
172
+
173
+ if (!searchQuery.value) return (dynamicOptions.value = originalOptions.value)
174
+
175
+ await new Promise((resolve) => setTimeout(resolve, 500))
176
+
177
+ dynamicOptions.value = dynamicOptions.value.filter((option: { name: string }) => {
178
+ return option.name.toLowerCase().includes(searchQuery.value.toLowerCase())
179
+ })
180
+
181
+ isLoading.value = false
182
+ }
183
+
184
+ const selectedAirlines = ref([])
185
+ return { args, selectedAirlines, dynamicOptions, handleSearch, isLoading }
186
+ },
187
+ template: `
188
+ <div style="padding: 20px;">
189
+ <CpMultiselect v-model="selectedAirlines" v-bind="args" :options="dynamicOptions" :is-loading="isLoading" @search="handleSearch">
190
+ <template #prefix>
191
+ <cp-partner-badge type="airline" size="xs" />
192
+ </template>
193
+ <template #selected-option-leading-icon="{ option }">
194
+ <cp-airline-logo :iata-code="option.iata_code" size="14" />
195
+ </template>
196
+ <template #option="{ option }">
197
+ <div style="display: flex; align-items: center; gap: 8px;">
198
+ <cp-airline-logo :iata-code="option.iata_code" size="14" />
199
+ {{ option.name }}
200
+ </div>
201
+ </template>
202
+ </CpMultiselect>
203
+ </div>
204
+ `,
205
+ }),
206
+ }
207
+
208
+ export const Invalid: Story = {
209
+ args: {
210
+ placeholder: 'Select a supplier',
211
+ disabled: false,
212
+ isInvalid: true,
213
+ options: [],
214
+ },
215
+ render: (args) => ({
216
+ components: { CpMultiselect },
217
+ setup() {
218
+ const selectedSupplier = ref(null)
219
+ return { args, selectedSupplier }
220
+ },
221
+ template: `
222
+ <div style="padding: 20px;">
223
+ <CpMultiselect v-model="selectedSupplier" v-bind="args">
224
+ <template #prefix>
225
+ <cp-partner-badge type="supplier" size="xs" />
226
+ </template>
227
+ <template #option="{ option }">
228
+ <div style="display: flex; align-items: center; gap: 8px;">
229
+ <cp-partner-badge type="supplier" size="xs" />
230
+ {{ option.name }}
231
+ </div>
232
+ </template>
233
+ </CpMultiselect>
234
+ </div>
235
+ `,
236
+ }),
237
+ }