@kiva/kv-components 3.107.0 → 3.107.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.
Files changed (177) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/components/.storybook/main.js +85 -0
  3. package/dist/components/.storybook/package.json +3 -0
  4. package/dist/components/.storybook/preview.js +61 -0
  5. package/dist/components/.storybook/tailwind.css +5 -0
  6. package/dist/components/KvAccordionItem.vue +130 -0
  7. package/dist/components/KvActivityRow.vue +33 -0
  8. package/dist/components/KvBorrowerImage.vue +179 -0
  9. package/dist/components/KvButton.vue +287 -0
  10. package/dist/components/KvCarousel.vue +297 -0
  11. package/dist/components/KvCartModal.vue +365 -0
  12. package/dist/components/KvCheckbox.vue +203 -0
  13. package/dist/components/KvChip.vue +54 -0
  14. package/dist/components/KvClassicLoanCard.vue +527 -0
  15. package/dist/components/KvCommentsAdd.vue +135 -0
  16. package/dist/components/KvCommentsContainer.vue +84 -0
  17. package/dist/components/KvCommentsHeartButton.vue +70 -0
  18. package/dist/components/KvCommentsList.vue +68 -0
  19. package/dist/components/KvCommentsListItem.vue +241 -0
  20. package/dist/components/KvCommentsReplyButton.vue +52 -0
  21. package/dist/components/KvContentfulImg.vue +273 -0
  22. package/dist/components/KvCountdownTimer.vue +59 -0
  23. package/dist/components/KvExpandable.vue +84 -0
  24. package/dist/components/KvExpandableQuestion.vue +120 -0
  25. package/dist/components/KvFlag.vue +120 -0
  26. package/dist/components/KvGrid.vue +28 -0
  27. package/dist/components/KvImpactDashboardHeader.vue +40 -0
  28. package/dist/components/KvInlineActivityCard.vue +55 -0
  29. package/dist/components/KvInlineActivityFeed.vue +38 -0
  30. package/dist/components/KvIntroductionLoanCard.vue +446 -0
  31. package/dist/components/KvLendAmountButton.vue +65 -0
  32. package/dist/components/KvLendCta.vue +451 -0
  33. package/dist/components/KvLightbox.vue +334 -0
  34. package/dist/components/KvLineGraph.vue +128 -0
  35. package/dist/components/KvLoadingPlaceholder.vue +38 -0
  36. package/dist/components/KvLoadingSpinner.vue +81 -0
  37. package/dist/components/KvLoanActivities.vue +268 -0
  38. package/dist/components/KvLoanBookmark.vue +39 -0
  39. package/dist/components/KvLoanCallouts.vue +53 -0
  40. package/dist/components/KvLoanProgressGroup.vue +76 -0
  41. package/dist/components/KvLoanTag.vue +88 -0
  42. package/dist/components/KvLoanTeamPick.vue +44 -0
  43. package/dist/components/KvLoanUse.vue +92 -0
  44. package/dist/components/KvMap.vue +599 -0
  45. package/dist/components/KvMaterialIcon.vue +47 -0
  46. package/dist/components/KvPageContainer.vue +15 -0
  47. package/dist/components/KvPagination.vue +198 -0
  48. package/dist/components/KvPieChart.vue +257 -0
  49. package/dist/components/KvPopper.vue +178 -0
  50. package/dist/components/KvProgressBar.vue +149 -0
  51. package/dist/components/KvRadio.vue +198 -0
  52. package/dist/components/KvSelect.vue +114 -0
  53. package/dist/components/KvSideSheet.vue +134 -0
  54. package/dist/components/KvSwitch.vue +143 -0
  55. package/dist/components/KvTab.vue +90 -0
  56. package/dist/components/KvTabPanel.vue +64 -0
  57. package/dist/components/KvTabs.vue +182 -0
  58. package/dist/components/KvTextInput.vue +247 -0
  59. package/dist/components/KvTextLink.vue +138 -0
  60. package/dist/components/KvThemeProvider.vue +122 -0
  61. package/dist/components/KvToast.vue +221 -0
  62. package/dist/components/KvTooltip.vue +168 -0
  63. package/dist/components/KvTreeMapChart.vue +229 -0
  64. package/dist/components/KvUserAvatar.vue +132 -0
  65. package/dist/components/KvVerticalCarousel.vue +156 -0
  66. package/dist/components/KvVotingCard.vue +160 -0
  67. package/dist/components/KvVotingCardV2.vue +154 -0
  68. package/dist/components/KvWideLoanCard.vue +432 -0
  69. package/dist/components/stories/Forms.stories.js +62 -0
  70. package/dist/components/stories/KvAccordionItem.stories.js +24 -0
  71. package/dist/components/stories/KvActivityRow.stories.js +25 -0
  72. package/dist/components/stories/KvBorrowerImage.stories.js +68 -0
  73. package/dist/components/stories/KvButton.stories.js +144 -0
  74. package/dist/components/stories/KvCarousel.stories.js +426 -0
  75. package/dist/components/stories/KvCartModal.stories.js +54 -0
  76. package/dist/components/stories/KvCheckbox.stories.js +163 -0
  77. package/dist/components/stories/KvChip.stories.js +43 -0
  78. package/dist/components/stories/KvClassicLoanCard.stories.js +480 -0
  79. package/dist/components/stories/KvCommentsAdd.stories.js +32 -0
  80. package/dist/components/stories/KvCommentsContainer.stories.js +42 -0
  81. package/dist/components/stories/KvCommentsHeartButton.stories.js +25 -0
  82. package/dist/components/stories/KvCommentsList.stories.js +39 -0
  83. package/dist/components/stories/KvCommentsListItem.stories.js +45 -0
  84. package/dist/components/stories/KvCommentsReplyButton.stories.js +21 -0
  85. package/dist/components/stories/KvContentfulImg.stories.js +196 -0
  86. package/dist/components/stories/KvCountdownTimer.stories.js +30 -0
  87. package/dist/components/stories/KvExpandableQuestion.stories.js +129 -0
  88. package/dist/components/stories/KvFlag.stories.js +36 -0
  89. package/dist/components/stories/KvGrid.stories.js +97 -0
  90. package/dist/components/stories/KvImpactDashboardHeader.stories.js +22 -0
  91. package/dist/components/stories/KvInlineActivityCard.stories.js +69 -0
  92. package/dist/components/stories/KvInlineActivityFeed.stories.js +76 -0
  93. package/dist/components/stories/KvIntroductionLoanCard.stories.js +208 -0
  94. package/dist/components/stories/KvLendAmountButton.stories.js +31 -0
  95. package/dist/components/stories/KvLendCta.stories.js +177 -0
  96. package/dist/components/stories/KvLightbox.stories.js +304 -0
  97. package/dist/components/stories/KvLineGraph.stories.js +52 -0
  98. package/dist/components/stories/KvLoadingPlaceholder.stories.js +17 -0
  99. package/dist/components/stories/KvLoadingSpinner.stories.js +52 -0
  100. package/dist/components/stories/KvLoanActivities.stories.js +104 -0
  101. package/dist/components/stories/KvLoanBookmark.stories.js +22 -0
  102. package/dist/components/stories/KvLoanCallouts.stories.js +22 -0
  103. package/dist/components/stories/KvLoanProgressGroup.stories.js +29 -0
  104. package/dist/components/stories/KvLoanTag.stories.js +61 -0
  105. package/dist/components/stories/KvLoanTeamPick.stories.js +20 -0
  106. package/dist/components/stories/KvLoanUse.stories.js +60 -0
  107. package/dist/components/stories/KvMap.stories.js +121 -0
  108. package/dist/components/stories/KvMaterialIcon.stories.js +201 -0
  109. package/dist/components/stories/KvPageContainer.stories.js +50 -0
  110. package/dist/components/stories/KvPagination.stories.js +70 -0
  111. package/dist/components/stories/KvPieChart.stories.js +47 -0
  112. package/dist/components/stories/KvProgressBar.stories.js +53 -0
  113. package/dist/components/stories/KvRadio.stories.js +140 -0
  114. package/dist/components/stories/KvSelect.stories.js +125 -0
  115. package/dist/components/stories/KvSideSheet.stories.js +50 -0
  116. package/dist/components/stories/KvSwitch.stories.js +66 -0
  117. package/dist/components/stories/KvTabs.stories.js +106 -0
  118. package/dist/components/stories/KvTextInput.stories.js +194 -0
  119. package/dist/components/stories/KvTextLink.stories.js +55 -0
  120. package/dist/components/stories/KvThemeProvider.stories.js +178 -0
  121. package/dist/components/stories/KvToast.stories.js +117 -0
  122. package/dist/components/stories/KvTooltip.stories.js +26 -0
  123. package/dist/components/stories/KvTreeMapChart.stories.js +42 -0
  124. package/dist/components/stories/KvUserAvatar.stories.js +47 -0
  125. package/dist/components/stories/KvVerticalCarousel.stories.js +168 -0
  126. package/dist/components/stories/KvVotingCard.stories.js +33 -0
  127. package/dist/components/stories/KvVotingCardV2.stories.js +89 -0
  128. package/dist/components/stories/KvWideLoanCard.stories.js +292 -0
  129. package/dist/components/stories/StyleguidePrimitives.stories.js +499 -0
  130. package/dist/components/stories/StyleguideProse.stories.js +215 -0
  131. package/dist/data/countries-borders.json +1 -0
  132. package/dist/data/ne_110m_admin_0_countries.json +1 -0
  133. package/dist/utils/Alea.js +9 -0
  134. package/dist/utils/attrs.js +7 -0
  135. package/dist/utils/carousels.js +8 -0
  136. package/dist/{attrs.js → utils/chunk-3HK4G4NT.js} +1 -0
  137. package/dist/{loanCard.js → utils/chunk-55HF2ORX.js} +1 -0
  138. package/dist/{expander.js → utils/chunk-AY3PR5S4.js} +3 -2
  139. package/dist/{carousels.js → utils/chunk-AZPWOFD5.js} +1 -0
  140. package/dist/{printing.js → utils/chunk-B5J5WLAH.js} +1 -0
  141. package/dist/{Alea.js → utils/chunk-GPSH6OPA.js} +2 -1
  142. package/dist/{scrollLock.js → utils/chunk-HIY5IW65.js} +2 -1
  143. package/dist/{treemap.js → utils/chunk-MSMZIN54.js} +1 -0
  144. package/dist/{imageUtils.js → utils/chunk-OXJCCNNW.js} +1 -0
  145. package/dist/{touchEvents.js → utils/chunk-S3MABILA.js} +3 -2
  146. package/dist/{mapUtils.js → utils/chunk-VIGEMAKO.js} +5 -4
  147. package/dist/utils/chunk-YCNMJ4YV.js +37 -0
  148. package/dist/{loanUtils.js → utils/chunk-YFEC5ODJ.js} +7 -6
  149. package/dist/utils/expander.js +9 -0
  150. package/dist/utils/imageUtils.js +9 -0
  151. package/dist/utils/index.cjs +1118 -0
  152. package/dist/utils/index.js +166 -0
  153. package/dist/utils/loanCard.js +9 -0
  154. package/dist/utils/loanUtils.js +23 -0
  155. package/dist/utils/mapUtils.js +15 -0
  156. package/dist/utils/printing.js +9 -0
  157. package/dist/utils/scrollLock.js +13 -0
  158. package/dist/{throttle.js → utils/throttle.js} +1 -0
  159. package/dist/utils/touchEvents.js +11 -0
  160. package/dist/utils/treemap.js +7 -0
  161. package/package.json +7 -7
  162. package/utils/index.js +14 -0
  163. package/index.js +0 -3
  164. /package/dist/{Alea.cjs → utils/Alea.cjs} +0 -0
  165. /package/dist/{attrs.cjs → utils/attrs.cjs} +0 -0
  166. /package/dist/{carousels.cjs → utils/carousels.cjs} +0 -0
  167. /package/dist/{chunk-HV3AUBFT.js → utils/chunk-HV3AUBFT.js} +0 -0
  168. /package/dist/{expander.cjs → utils/expander.cjs} +0 -0
  169. /package/dist/{imageUtils.cjs → utils/imageUtils.cjs} +0 -0
  170. /package/dist/{loanCard.cjs → utils/loanCard.cjs} +0 -0
  171. /package/dist/{loanUtils.cjs → utils/loanUtils.cjs} +0 -0
  172. /package/dist/{mapUtils.cjs → utils/mapUtils.cjs} +0 -0
  173. /package/dist/{printing.cjs → utils/printing.cjs} +0 -0
  174. /package/dist/{scrollLock.cjs → utils/scrollLock.cjs} +0 -0
  175. /package/dist/{throttle.cjs → utils/throttle.cjs} +0 -0
  176. /package/dist/{touchEvents.cjs → utils/touchEvents.cjs} +0 -0
  177. /package/dist/{treemap.cjs → utils/treemap.cjs} +0 -0
@@ -0,0 +1,273 @@
1
+ <template>
2
+ <figure
3
+ v-if="(width || height) && contentfulSrc"
4
+ class="tw-inline-block tw-not-prose"
5
+ >
6
+ <picture
7
+ class="tw-h-full tw-w-full"
8
+ style="object-fit: inherit;"
9
+ >
10
+ <!-- Set of image sources -->
11
+ <template v-if="sourceSizes.length > 0">
12
+ <!-- browser supports webp -->
13
+ <source
14
+ v-for="(image, index) in sourceSizes"
15
+ :key="'webp-image'+index"
16
+ :media="'('+image.media+')'"
17
+ type="image/webp"
18
+ :width="image.width ? image.width : null"
19
+ :height="image.height ? image.height : null"
20
+ :srcset="`
21
+ ${buildUrl(image, 2)}&fit=${fit}&f=${focus}&fm=webp&q=${setQuality(image.width, '2x')} 2x,
22
+ ${buildUrl(image)}&fit=${fit}&f=${focus}&fm=webp&q=${setQuality(image.width, '1x')} 1x`"
23
+ >
24
+ <!-- browser doesn't support webp -->
25
+ <!-- eslint-disable max-len -->
26
+ <source
27
+ v-for="(image, index) in sourceSizes"
28
+ :key="'fallback-image'+index"
29
+ :media="'('+image.media+')'"
30
+ :width="image.width ? image.width : null"
31
+ :height="image.height ? image.height : null"
32
+ :srcset="`
33
+ ${buildUrl(image, 2)}&fit=${fit}&f=${focus}&fm=${fallbackFormat}&q=${setQuality(image.width, '2x')} 2x,
34
+ ${buildUrl(image)}&fit=${fit}&f=${focus}&fm=${fallbackFormat}&q=${setQuality(image.width, '1x')} 1x`"
35
+ >
36
+ <!-- eslint-enable max-len -->
37
+ <!-- browser doesn't support picture element -->
38
+ <!-- eslint-disable max-len -->
39
+ <img
40
+ class="tw-max-w-full tw-max-h-full"
41
+ style="width: inherit; height: inherit; object-fit: inherit;"
42
+ :src="`${buildUrl(width, height)}&fit=${fit}&f=${focus}&fm=${fallbackFormat}&q=${setQuality(width, '1x')}`"
43
+ :alt="caption || alt"
44
+ :loading="loading"
45
+ >
46
+ <!-- eslint-enable max-len -->
47
+ </template>
48
+
49
+ <!-- Single image -->
50
+ <template v-if="sourceSizes.length === 0">
51
+ <!-- browser supports webp -->
52
+ <source
53
+ type="image/webp"
54
+ :srcset="`
55
+ ${buildUrl(null, 2)}&fit=${fit}&f=${focus}&fm=webp&q=${setQuality(width, '2x')} 2x,
56
+ ${buildUrl()}&fit=${fit}&f=${focus}&fm=webp&q=${setQuality(width, '1x')} 1x`"
57
+ >
58
+ <!-- browser doesn't support webp or browser doesn't support picture element -->
59
+ <img
60
+ class="tw-max-w-full tw-max-h-full"
61
+ style="width: inherit; height: inherit; object-fit: inherit;"
62
+ :srcset="`
63
+ ${buildUrl(null, 2)}&fit=${fit}&f=${focus}&fm=${fallbackFormat}&q=${setQuality(width, '2x')} 2x,
64
+ ${buildUrl()}&fit=${fit}&f=${focus}&fm=${fallbackFormat}&q=${setQuality(width, '1x')} 1x`"
65
+ :src="`${buildUrl()}&fit=${fit}&f=${focus}&fm=${fallbackFormat}&q=${setQuality(width, '1x')}`"
66
+ :width="width ? width : null"
67
+ :height="height ? height : null"
68
+ :alt="caption || alt"
69
+ :loading="loading"
70
+ >
71
+ </template>
72
+ </picture>
73
+ <figcaption
74
+ v-if="caption"
75
+ class="tw-text-h4 tw-mt-2"
76
+ >
77
+ <span
78
+ class="tw-inline-flex tw-align-text-top"
79
+ aria-hidden="true"
80
+ role="img"
81
+ >
82
+ <svg
83
+ class="tw-h-2 tw-w-2.5"
84
+ viewBox="0 0 20 20"
85
+ xmlns="http://www.w3.org/2000/svg"
86
+ >
87
+ <!-- eslint-disable max-len -->
88
+ <path
89
+ d="m3.12088 18.7441c12.86212 0 15.87912-14.52631 15.87912-16.99996h-1.1209c-12.85272 0-15.8791 14.51376-15.8791 16.99996z"
90
+ fill="currentColor"
91
+ />
92
+ <!-- eslint-enable max-len -->
93
+ </svg>
94
+ </span>
95
+ {{ caption }}
96
+ </figcaption>
97
+ </figure>
98
+ </template>
99
+
100
+ <script>
101
+ import { computed, toRefs } from 'vue-demi';
102
+ // Since it's easy for marketing or other to upload massive images to contentful,
103
+ // in order to be performant respectful of our users data plans, and not damage
104
+ // our SEO, we shouldn't send the source image directly to our users.
105
+
106
+ // This component uses to contentful's image query params to:
107
+ // Serve webp with a fallback for older browsers.
108
+ // Offer both 1x and 2x images.
109
+ // Properly size the images and make sure they're compressed.
110
+ // Crop images around focus area.
111
+ // If we have both width and height we pass those attributes to the image to prevent jank.
112
+ // Allow lazy loading via image attribute.
113
+
114
+ export default {
115
+ props: {
116
+ /**
117
+ * Large, uncompressed image url that you get back from contentful.
118
+ * */
119
+ contentfulSrc: {
120
+ type: String,
121
+ required: true,
122
+ },
123
+ /**
124
+ * If the browser can't support webp we fallback to this image format.
125
+ * `jpg, png, webp`
126
+ * */
127
+ fallbackFormat: {
128
+ type: String,
129
+ default: 'jpg',
130
+ validator(value) {
131
+ // The value must match one of these strings
132
+ return ['jpg', 'png', 'gif'].indexOf(value) !== -1;
133
+ },
134
+ },
135
+ /**
136
+ * 1x width of the image. Width or height must be defined. Ideally both.
137
+ * */
138
+ width: {
139
+ type: [String, Number],
140
+ default: null,
141
+ },
142
+ /**
143
+ * 1x height of the image. Width or height must be defined. Ideally both.
144
+ * */
145
+ height: {
146
+ type: [String, Number],
147
+ default: null,
148
+ },
149
+ /**
150
+ * Alt text for the image
151
+ * */
152
+ alt: {
153
+ type: String,
154
+ default: '',
155
+ },
156
+ /**
157
+ * Loading hint to the browser - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading
158
+ * `lazy, eager`
159
+ * */
160
+ loading: {
161
+ type: String,
162
+ default: null,
163
+ validator(value) {
164
+ // The value must match one of these strings
165
+ return ['lazy', 'eager'].indexOf(value) !== -1;
166
+ },
167
+ },
168
+ /**
169
+ * Focus area when modifying images. Focus area has no effect on the default 'scale' fit.
170
+ * https://www.contentful.com/developers/docs/references/images-api/#/reference/resizing-&-cropping/specify-focus-area
171
+ * */
172
+ focus: {
173
+ type: String,
174
+ default: 'center',
175
+ validator(value) {
176
+ // The value must match one of these strings
177
+ // eslint-disable-next-line max-len
178
+ return ['center', 'top', 'right', 'left', 'bottom', 'top_right', 'top_left', 'bottom_right', 'bottom_left', 'face', 'faces'].indexOf(value) !== -1;
179
+ },
180
+ },
181
+ /**
182
+ * Focus area when modifying images. Focus area has no effect on the default 'scale' fit.
183
+ * https://www.contentful.com/developers/docs/references/images-api/#/reference/resizing-&-cropping/change-the-resizing-behavior
184
+ * */
185
+ fit: {
186
+ type: String,
187
+ default: 'scale',
188
+ validator(value) {
189
+ // The value must match one of these strings
190
+ return ['pad', 'fill', 'scale', 'crop', 'thumb'].indexOf(value) !== -1;
191
+ },
192
+ },
193
+ /**
194
+ * Sources sizes.
195
+ * Array of objects for different image sources.
196
+ * Sample object:
197
+ * {
198
+ width: 1440,
199
+ height: 620,
200
+ media: 'min-width: 1025px',
201
+ url: '//some-protocol-relative-contentful-url'
202
+ }
203
+ * */
204
+ sourceSizes: {
205
+ type: Array,
206
+ required: false,
207
+ default: () => [],
208
+ },
209
+ },
210
+ setup(props) {
211
+ const {
212
+ alt,
213
+ contentfulSrc,
214
+ width,
215
+ height,
216
+ } = toRefs(props);
217
+
218
+ const buildUrl = (image = null, multiplier = 1) => {
219
+ let src = image && image.url ? `${image.url}?` : `${contentfulSrc.value}?`;
220
+ let imgWidth = image ? image.width : width.value;
221
+ let imgHeight = image ? image.height : height.value;
222
+ // The max contentful image size is 4000px so we have to
223
+ // impose a limit of 2000px here for both height and width
224
+ // so when we request the retina x2 image we don't go over the 4000px limit
225
+ let newMultiplier;
226
+ if (imgWidth >= 2000) {
227
+ newMultiplier = imgWidth / 1999;
228
+ imgWidth = 1999;
229
+ imgHeight = Math.round(imgHeight / newMultiplier);
230
+ }
231
+ if (imgHeight >= 2000) {
232
+ newMultiplier = imgHeight / 1999;
233
+ imgHeight = 1999;
234
+ imgWidth = Math.round(imgWidth / newMultiplier);
235
+ }
236
+ if (imgWidth) {
237
+ src += `w=${imgWidth * multiplier}`;
238
+ }
239
+ if (imgWidth && imgHeight) {
240
+ src += '&';
241
+ }
242
+ if (imgHeight) {
243
+ src += `h=${imgHeight * multiplier}`;
244
+ }
245
+ return src;
246
+ };
247
+
248
+ const setQuality = (imgWidth, imgScale) => {
249
+ if (imgScale === '2x') {
250
+ return 65;
251
+ }
252
+ // Smaller images show a marked degradation at 80 quality so we bump it up to 95
253
+ if (imgWidth && parseInt(imgWidth, 10) < 200) {
254
+ return 95;
255
+ }
256
+ return 80;
257
+ };
258
+
259
+ const caption = computed(() => {
260
+ if (alt.value && alt.value.charAt(0) === '^') {
261
+ return alt.value.slice(1).trim();
262
+ }
263
+ return '';
264
+ });
265
+
266
+ return {
267
+ buildUrl,
268
+ caption,
269
+ setQuality,
270
+ };
271
+ },
272
+ };
273
+ </script>
@@ -0,0 +1,59 @@
1
+ <template>
2
+ <span v-if="timeLeft">
3
+ {{ remainingHours }}h {{ timeLeft.minutes() }}m {{ timeLeft.seconds() }}s
4
+ </span>
5
+ </template>
6
+
7
+ <script>
8
+ import {
9
+ ref,
10
+ toRefs,
11
+ onBeforeUnmount,
12
+ onMounted,
13
+ computed,
14
+ } from 'vue-demi';
15
+ import moment from 'moment';
16
+
17
+ export default {
18
+ props: {
19
+ msLeft: {
20
+ type: Number,
21
+ required: true,
22
+ },
23
+ },
24
+ setup(props) {
25
+ const { msLeft } = toRefs(props);
26
+
27
+ const interval = ref(null);
28
+ const timeLeft = ref(null);
29
+
30
+ const remainingHours = computed(() => {
31
+ return Math.floor(timeLeft.value.asHours());
32
+ });
33
+
34
+ onMounted(() => {
35
+ timeLeft.value = moment.duration(msLeft.value > 0 ? msLeft.value : 0, 'milliseconds');
36
+
37
+ if (timeLeft.value > 0) {
38
+ const countdownInterval = 1000;
39
+
40
+ interval.value = setInterval(() => {
41
+ timeLeft.value = moment.duration(timeLeft.value - countdownInterval, 'milliseconds');
42
+
43
+ if (timeLeft.value <= 0) {
44
+ clearInterval(interval.value);
45
+ }
46
+ }, countdownInterval);
47
+ }
48
+ });
49
+
50
+ onBeforeUnmount(() => {
51
+ if (interval.value) {
52
+ clearInterval(interval.value);
53
+ }
54
+ });
55
+
56
+ return { timeLeft, remainingHours };
57
+ },
58
+ };
59
+ </script>
@@ -0,0 +1,84 @@
1
+ <template>
2
+ <transition
3
+ @enter="enter"
4
+ @leave="leave"
5
+ >
6
+ <slot></slot>
7
+ </transition>
8
+ </template>
9
+
10
+ <script>
11
+ import {
12
+ toRefs,
13
+ } from 'vue-demi';
14
+ import { expand, collapse } from '../utils/expander';
15
+
16
+ export default {
17
+ props: {
18
+ property: {
19
+ type: String,
20
+ default: 'height',
21
+ },
22
+ delay: {
23
+ type: Number,
24
+ default: 500,
25
+ },
26
+ easing: {
27
+ type: String,
28
+ default: 'ease',
29
+ },
30
+ skipEnter: {
31
+ type: Boolean,
32
+ default: false,
33
+ },
34
+ skipLeave: {
35
+ type: Boolean,
36
+ default: false,
37
+ },
38
+ },
39
+ setup(props) {
40
+ const {
41
+ property,
42
+ delay,
43
+ easing,
44
+ skipEnter,
45
+ skipLeave,
46
+ } = toRefs(props);
47
+
48
+ const enter = (el, done) => {
49
+ if (skipEnter.value) {
50
+ return done();
51
+ }
52
+
53
+ expand(el, {
54
+ property: property.value,
55
+ delay: delay.value,
56
+ easing: easing.value,
57
+ done,
58
+ });
59
+
60
+ return true;
61
+ };
62
+
63
+ const leave = (el, done) => {
64
+ if (skipLeave.value) {
65
+ return done();
66
+ }
67
+
68
+ collapse(el, {
69
+ property: property.value,
70
+ delay: delay.value,
71
+ easing: easing.value,
72
+ done,
73
+ });
74
+
75
+ return true;
76
+ };
77
+
78
+ return {
79
+ enter,
80
+ leave,
81
+ };
82
+ },
83
+ };
84
+ </script>
@@ -0,0 +1,120 @@
1
+ <template>
2
+ <div class="tw-whitespace-normal">
3
+ <button
4
+ class="tw-w-full tw-py-2 tw-flex tw-justify-between tw-not-prose"
5
+ @click="toggleFAQ"
6
+ >
7
+ <h3 class="tw-text-subhead tw-text-left">
8
+ {{ title }}
9
+ </h3>
10
+ <kv-material-icon
11
+ class="tw-w-4 tw-h-4"
12
+ :icon="open ? mdiChevronUp : mdiChevronDown"
13
+ @click.stop="toggleFAQ"
14
+ />
15
+ </button>
16
+ <kv-expandable easing="ease-in-out">
17
+ <div v-show="open">
18
+ <div class="tw-prose tw-pb-4 tw-pt-2">
19
+ <slot></slot>
20
+ <!-- eslint-disable vue/no-v-html -->
21
+ <div
22
+ v-if="content !== ''"
23
+ v-html="content"
24
+ >
25
+ </div>
26
+ <!--eslint-enable-->
27
+ </div>
28
+ </div>
29
+ </kv-expandable>
30
+ </div>
31
+ </template>
32
+
33
+ <script>
34
+ import {
35
+ computed,
36
+ onMounted,
37
+ watch,
38
+ ref,
39
+ toRefs,
40
+ } from 'vue-demi';
41
+ import {
42
+ mdiChevronDown,
43
+ mdiChevronUp,
44
+ } from '@mdi/js';
45
+ import { paramCase } from 'change-case';
46
+
47
+ import KvExpandable from './KvExpandable.vue';
48
+ import KvMaterialIcon from './KvMaterialIcon.vue';
49
+
50
+ export default {
51
+ components: {
52
+ KvMaterialIcon,
53
+ KvExpandable,
54
+ },
55
+ props: {
56
+ /**
57
+ * Question Title
58
+ * */
59
+ title: {
60
+ type: String,
61
+ default: '',
62
+ },
63
+ /**
64
+ * Question Content - can accept raw html
65
+ * */
66
+ content: {
67
+ type: String,
68
+ default: '',
69
+ },
70
+ active: {
71
+ type: Boolean,
72
+ default: false,
73
+ },
74
+ routeHash: {
75
+ type: String,
76
+ default: '',
77
+ },
78
+ kvTrackFunction: {
79
+ type: Function,
80
+ default: () => {},
81
+ },
82
+ },
83
+ emits: [
84
+ 'toggle',
85
+ ],
86
+ setup(props, { emit }) {
87
+ const {
88
+ title,
89
+ active,
90
+ } = toRefs(props);
91
+ const open = ref(active.value || false);
92
+ const titleSlugified = computed(() => paramCase(title.value));
93
+
94
+ const toggleFAQ = () => {
95
+ props.kvTrackFunction('faq', 'toggle', titleSlugified.value, open.value ? 'expand' : 'collapse');
96
+ open.value = !open.value;
97
+ emit('toggle', { title: titleSlugified.value });
98
+ };
99
+
100
+ watch(active, (val) => {
101
+ open.value = val;
102
+ });
103
+
104
+ onMounted(() => {
105
+ /** Allows directly linking to the question via a hash equal to slugified title */
106
+ if (props.routeHash === `#${titleSlugified.value}`) {
107
+ open.value = true;
108
+ }
109
+ });
110
+
111
+ return {
112
+ open,
113
+ mdiChevronDown,
114
+ mdiChevronUp,
115
+ titleSlugified,
116
+ toggleFAQ,
117
+ };
118
+ },
119
+ };
120
+ </script>
@@ -0,0 +1,120 @@
1
+ <template>
2
+ <div
3
+ v-if="country"
4
+ :class="`kv-flag kv-flag--${aspectRatio}`"
5
+ :style="{ maxWidth: spriteWidth, minWidth: spriteWidth }"
6
+ >
7
+ <div
8
+ class="
9
+ kv-flag__wrapper
10
+ kv-flag-svg
11
+ tw-bg-gray-100
12
+ tw-relative
13
+ tw-overflow-hidden
14
+ tw-h-0
15
+ tw-w-full
16
+ tw-border
17
+ tw-border-gray-600
18
+ !tw-bg-cover
19
+ fib
20
+ "
21
+ :class="classes"
22
+ >
23
+ <span class="tw-sr-only">{{ countryName }}</span>
24
+ </div>
25
+ <span
26
+ v-if="showName"
27
+ class="tw-text-h4 tw-my-2"
28
+ >{{ getNameByCode(country) }}</span>
29
+ </div>
30
+ </template>
31
+
32
+ <script>
33
+ export default {
34
+ name: 'KvFlag',
35
+ props: {
36
+ /**
37
+ * 2 letter ISO country code of the flag to show
38
+ * */
39
+ country: {
40
+ type: String,
41
+ required: true,
42
+ },
43
+ /**
44
+ * Aspect Ratio of the flag image
45
+ * `4x3, 1x1`
46
+ * */
47
+ aspectRatio: {
48
+ type: String,
49
+ default: '4x3',
50
+ },
51
+ /**
52
+ * Show the name of the country next to the flag
53
+ */
54
+ showName: {
55
+ type: Boolean,
56
+ default: false,
57
+ },
58
+ /**
59
+ * Override the width of the flag
60
+ */
61
+ widthOverride: {
62
+ type: String,
63
+ default: null,
64
+ },
65
+ /**
66
+ * Hide the border around the flag
67
+ */
68
+ hideBorder: {
69
+ type: Boolean,
70
+ default: false,
71
+ },
72
+ },
73
+ async setup() {
74
+ const countryList = await import('~/node_modules/flag-icons/country.json');
75
+
76
+ return {
77
+ countryList,
78
+ };
79
+ },
80
+ computed: {
81
+ spriteWidth() {
82
+ if (this.widthOverride) {
83
+ return this.widthOverride;
84
+ }
85
+ return '100%';
86
+ },
87
+ countryName() {
88
+ return `Flag of ${this.getNameByCode(this.country)}`;
89
+ },
90
+ classes() {
91
+ return {
92
+ [`fi-${this.country.toLowerCase()}`]: true,
93
+ 'tw-border-0': this.hideBorder,
94
+ };
95
+ },
96
+ },
97
+ methods: {
98
+ getNameByCode(code) {
99
+ const uppercaseCode = code?.toLowerCase() ?? '';
100
+ return this.countryList?.default?.find((country) => country.code === uppercaseCode)?.name ?? '';
101
+ },
102
+ },
103
+ };
104
+ </script>
105
+
106
+ <style lang="postcss" scoped>
107
+ @import "~/node_modules/flag-icons/css/flag-icons.min.css";
108
+
109
+ .kv-flag__wrapper {
110
+ line-height: 0;
111
+ }
112
+
113
+ .kv-flag--4x3 .kv-flag__wrapper.kv-flag-svg {
114
+ padding-bottom: 71%;
115
+ }
116
+
117
+ .kv-flag--1x1 .kv-flag__wrapper.kv-flag-svg {
118
+ padding-bottom: 96%;
119
+ }
120
+ </style>
@@ -0,0 +1,28 @@
1
+ <template>
2
+ <component
3
+ :is="as"
4
+ class="tw-grid tw-gap-2 md:tw-gap-3 lg:tw-gap-3.5"
5
+ >
6
+ <slot></slot>
7
+ </component>
8
+ </template>
9
+
10
+ <script>
11
+ /**
12
+ * A light wrapper around Tailwind's grid class, setting the gaps as specified by design.
13
+ * To use, specify the number of columns as a tailwind class on this component.
14
+ * e.g., `<kv-grid class="tw-grid-cols-6 md:tw-grid-cols-12"> grid children here... </kv-grid>`
15
+ */
16
+ export default {
17
+ props: {
18
+ /**
19
+ * Element name of the grid container element.
20
+ * e.g., <kv-grid as="ul"><li>list item... renders <ul><li>list item...
21
+ * */
22
+ as: {
23
+ type: String,
24
+ default: 'div',
25
+ },
26
+ },
27
+ };
28
+ </script>