@kiva/kv-components 3.106.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 (179) hide show
  1. package/.eslintrc.cjs +1 -0
  2. package/CHANGELOG.md +22 -0
  3. package/dist/components/.storybook/main.js +85 -0
  4. package/dist/components/.storybook/package.json +3 -0
  5. package/dist/components/.storybook/preview.js +61 -0
  6. package/dist/components/.storybook/tailwind.css +5 -0
  7. package/dist/components/KvAccordionItem.vue +130 -0
  8. package/dist/components/KvActivityRow.vue +33 -0
  9. package/dist/components/KvBorrowerImage.vue +179 -0
  10. package/dist/components/KvButton.vue +287 -0
  11. package/dist/components/KvCarousel.vue +297 -0
  12. package/dist/components/KvCartModal.vue +365 -0
  13. package/dist/components/KvCheckbox.vue +203 -0
  14. package/dist/components/KvChip.vue +54 -0
  15. package/dist/components/KvClassicLoanCard.vue +527 -0
  16. package/dist/components/KvCommentsAdd.vue +135 -0
  17. package/dist/components/KvCommentsContainer.vue +84 -0
  18. package/dist/components/KvCommentsHeartButton.vue +70 -0
  19. package/dist/components/KvCommentsList.vue +68 -0
  20. package/dist/components/KvCommentsListItem.vue +241 -0
  21. package/dist/components/KvCommentsReplyButton.vue +52 -0
  22. package/dist/components/KvContentfulImg.vue +273 -0
  23. package/dist/components/KvCountdownTimer.vue +59 -0
  24. package/dist/components/KvExpandable.vue +84 -0
  25. package/dist/components/KvExpandableQuestion.vue +120 -0
  26. package/dist/components/KvFlag.vue +120 -0
  27. package/dist/components/KvGrid.vue +28 -0
  28. package/dist/components/KvImpactDashboardHeader.vue +40 -0
  29. package/dist/components/KvInlineActivityCard.vue +55 -0
  30. package/dist/components/KvInlineActivityFeed.vue +38 -0
  31. package/dist/components/KvIntroductionLoanCard.vue +446 -0
  32. package/dist/components/KvLendAmountButton.vue +65 -0
  33. package/dist/components/KvLendCta.vue +451 -0
  34. package/dist/components/KvLightbox.vue +334 -0
  35. package/dist/components/KvLineGraph.vue +128 -0
  36. package/dist/components/KvLoadingPlaceholder.vue +38 -0
  37. package/dist/components/KvLoadingSpinner.vue +81 -0
  38. package/dist/components/KvLoanActivities.vue +268 -0
  39. package/dist/components/KvLoanBookmark.vue +39 -0
  40. package/dist/components/KvLoanCallouts.vue +53 -0
  41. package/dist/components/KvLoanProgressGroup.vue +76 -0
  42. package/dist/components/KvLoanTag.vue +88 -0
  43. package/dist/components/KvLoanTeamPick.vue +44 -0
  44. package/dist/components/KvLoanUse.vue +92 -0
  45. package/dist/components/KvMap.vue +599 -0
  46. package/dist/components/KvMaterialIcon.vue +47 -0
  47. package/dist/components/KvPageContainer.vue +15 -0
  48. package/dist/components/KvPagination.vue +198 -0
  49. package/dist/components/KvPieChart.vue +257 -0
  50. package/dist/components/KvPopper.vue +178 -0
  51. package/dist/components/KvProgressBar.vue +149 -0
  52. package/dist/components/KvRadio.vue +198 -0
  53. package/dist/components/KvSelect.vue +114 -0
  54. package/dist/components/KvSideSheet.vue +134 -0
  55. package/dist/components/KvSwitch.vue +143 -0
  56. package/dist/components/KvTab.vue +90 -0
  57. package/dist/components/KvTabPanel.vue +64 -0
  58. package/dist/components/KvTabs.vue +182 -0
  59. package/dist/components/KvTextInput.vue +247 -0
  60. package/dist/components/KvTextLink.vue +138 -0
  61. package/dist/components/KvThemeProvider.vue +122 -0
  62. package/dist/components/KvToast.vue +221 -0
  63. package/dist/components/KvTooltip.vue +168 -0
  64. package/dist/components/KvTreeMapChart.vue +229 -0
  65. package/dist/components/KvUserAvatar.vue +132 -0
  66. package/dist/components/KvVerticalCarousel.vue +156 -0
  67. package/dist/components/KvVotingCard.vue +160 -0
  68. package/dist/components/KvVotingCardV2.vue +154 -0
  69. package/dist/components/KvWideLoanCard.vue +432 -0
  70. package/dist/components/stories/Forms.stories.js +62 -0
  71. package/dist/components/stories/KvAccordionItem.stories.js +24 -0
  72. package/dist/components/stories/KvActivityRow.stories.js +25 -0
  73. package/dist/components/stories/KvBorrowerImage.stories.js +68 -0
  74. package/dist/components/stories/KvButton.stories.js +144 -0
  75. package/dist/components/stories/KvCarousel.stories.js +426 -0
  76. package/dist/components/stories/KvCartModal.stories.js +54 -0
  77. package/dist/components/stories/KvCheckbox.stories.js +163 -0
  78. package/dist/components/stories/KvChip.stories.js +43 -0
  79. package/dist/components/stories/KvClassicLoanCard.stories.js +480 -0
  80. package/dist/components/stories/KvCommentsAdd.stories.js +32 -0
  81. package/dist/components/stories/KvCommentsContainer.stories.js +42 -0
  82. package/dist/components/stories/KvCommentsHeartButton.stories.js +25 -0
  83. package/dist/components/stories/KvCommentsList.stories.js +39 -0
  84. package/dist/components/stories/KvCommentsListItem.stories.js +45 -0
  85. package/dist/components/stories/KvCommentsReplyButton.stories.js +21 -0
  86. package/dist/components/stories/KvContentfulImg.stories.js +196 -0
  87. package/dist/components/stories/KvCountdownTimer.stories.js +30 -0
  88. package/dist/components/stories/KvExpandableQuestion.stories.js +129 -0
  89. package/dist/components/stories/KvFlag.stories.js +36 -0
  90. package/dist/components/stories/KvGrid.stories.js +97 -0
  91. package/dist/components/stories/KvImpactDashboardHeader.stories.js +22 -0
  92. package/dist/components/stories/KvInlineActivityCard.stories.js +69 -0
  93. package/dist/components/stories/KvInlineActivityFeed.stories.js +76 -0
  94. package/dist/components/stories/KvIntroductionLoanCard.stories.js +208 -0
  95. package/dist/components/stories/KvLendAmountButton.stories.js +31 -0
  96. package/dist/components/stories/KvLendCta.stories.js +177 -0
  97. package/dist/components/stories/KvLightbox.stories.js +304 -0
  98. package/dist/components/stories/KvLineGraph.stories.js +52 -0
  99. package/dist/components/stories/KvLoadingPlaceholder.stories.js +17 -0
  100. package/dist/components/stories/KvLoadingSpinner.stories.js +52 -0
  101. package/dist/components/stories/KvLoanActivities.stories.js +104 -0
  102. package/dist/components/stories/KvLoanBookmark.stories.js +22 -0
  103. package/dist/components/stories/KvLoanCallouts.stories.js +22 -0
  104. package/dist/components/stories/KvLoanProgressGroup.stories.js +29 -0
  105. package/dist/components/stories/KvLoanTag.stories.js +61 -0
  106. package/dist/components/stories/KvLoanTeamPick.stories.js +20 -0
  107. package/dist/components/stories/KvLoanUse.stories.js +60 -0
  108. package/dist/components/stories/KvMap.stories.js +121 -0
  109. package/dist/components/stories/KvMaterialIcon.stories.js +201 -0
  110. package/dist/components/stories/KvPageContainer.stories.js +50 -0
  111. package/dist/components/stories/KvPagination.stories.js +70 -0
  112. package/dist/components/stories/KvPieChart.stories.js +47 -0
  113. package/dist/components/stories/KvProgressBar.stories.js +53 -0
  114. package/dist/components/stories/KvRadio.stories.js +140 -0
  115. package/dist/components/stories/KvSelect.stories.js +125 -0
  116. package/dist/components/stories/KvSideSheet.stories.js +50 -0
  117. package/dist/components/stories/KvSwitch.stories.js +66 -0
  118. package/dist/components/stories/KvTabs.stories.js +106 -0
  119. package/dist/components/stories/KvTextInput.stories.js +194 -0
  120. package/dist/components/stories/KvTextLink.stories.js +55 -0
  121. package/dist/components/stories/KvThemeProvider.stories.js +178 -0
  122. package/dist/components/stories/KvToast.stories.js +117 -0
  123. package/dist/components/stories/KvTooltip.stories.js +26 -0
  124. package/dist/components/stories/KvTreeMapChart.stories.js +42 -0
  125. package/dist/components/stories/KvUserAvatar.stories.js +47 -0
  126. package/dist/components/stories/KvVerticalCarousel.stories.js +168 -0
  127. package/dist/components/stories/KvVotingCard.stories.js +33 -0
  128. package/dist/components/stories/KvVotingCardV2.stories.js +89 -0
  129. package/dist/components/stories/KvWideLoanCard.stories.js +292 -0
  130. package/dist/components/stories/StyleguidePrimitives.stories.js +499 -0
  131. package/dist/components/stories/StyleguideProse.stories.js +215 -0
  132. package/dist/data/countries-borders.json +1 -0
  133. package/dist/data/ne_110m_admin_0_countries.json +1 -0
  134. package/dist/utils/Alea.cjs +87 -0
  135. package/dist/utils/Alea.js +9 -0
  136. package/dist/utils/attrs.cjs +50 -0
  137. package/dist/utils/attrs.js +7 -0
  138. package/dist/utils/carousels.cjs +184 -0
  139. package/dist/utils/carousels.js +8 -0
  140. package/dist/utils/chunk-3HK4G4NT.js +27 -0
  141. package/dist/utils/chunk-55HF2ORX.js +201 -0
  142. package/dist/utils/chunk-AY3PR5S4.js +54 -0
  143. package/dist/utils/chunk-AZPWOFD5.js +148 -0
  144. package/dist/utils/chunk-B5J5WLAH.js +18 -0
  145. package/dist/utils/chunk-GPSH6OPA.js +64 -0
  146. package/dist/utils/chunk-HIY5IW65.js +28 -0
  147. package/dist/utils/chunk-HV3AUBFT.js +15 -0
  148. package/dist/utils/chunk-MSMZIN54.js +110 -0
  149. package/dist/utils/chunk-OXJCCNNW.js +30 -0
  150. package/dist/utils/chunk-S3MABILA.js +22 -0
  151. package/dist/utils/chunk-VIGEMAKO.js +249 -0
  152. package/dist/utils/chunk-YCNMJ4YV.js +37 -0
  153. package/dist/utils/chunk-YFEC5ODJ.js +129 -0
  154. package/dist/utils/expander.cjs +78 -0
  155. package/dist/utils/expander.js +9 -0
  156. package/dist/utils/imageUtils.cjs +54 -0
  157. package/dist/utils/imageUtils.js +9 -0
  158. package/dist/utils/index.cjs +1118 -0
  159. package/dist/utils/index.js +166 -0
  160. package/dist/utils/loanCard.cjs +222 -0
  161. package/dist/utils/loanCard.js +9 -0
  162. package/dist/utils/loanUtils.cjs +170 -0
  163. package/dist/utils/loanUtils.js +23 -0
  164. package/dist/utils/mapUtils.cjs +276 -0
  165. package/dist/utils/mapUtils.js +15 -0
  166. package/dist/utils/printing.cjs +42 -0
  167. package/dist/utils/printing.js +9 -0
  168. package/dist/utils/scrollLock.cjs +54 -0
  169. package/dist/utils/scrollLock.js +13 -0
  170. package/dist/utils/throttle.cjs +38 -0
  171. package/dist/utils/throttle.js +7 -0
  172. package/dist/utils/touchEvents.cjs +47 -0
  173. package/dist/utils/touchEvents.js +11 -0
  174. package/dist/utils/treemap.cjs +133 -0
  175. package/dist/utils/treemap.js +7 -0
  176. package/package.json +12 -4
  177. package/utils/index.js +14 -0
  178. package/vue/KvVerticalCarousel.vue +1 -1
  179. package/index.js +0 -3
@@ -0,0 +1,334 @@
1
+ <template>
2
+ <transition
3
+ enter-active-class="tw-transition-opacity tw-duration-300"
4
+ leave-active-class="tw-transition-opacity tw-duration-300"
5
+ enter-class="tw-opacity-0"
6
+ enter-to-class="tw-opacity-full"
7
+ leave-class="tw-opacity-full"
8
+ leave-to-class="tw-opacity-0"
9
+ >
10
+ <!-- the screen -->
11
+ <div
12
+ v-show="visible"
13
+ class="
14
+ tw-z-modal
15
+ tw-fixed
16
+ tw-inset-0
17
+ tw-bg-black
18
+ tw-bg-opacity-[75%]
19
+ "
20
+ @click.stop.prevent="onScreenClick"
21
+ >
22
+ <div>
23
+ <div
24
+ class="
25
+ tw-flex
26
+ tw-absolute
27
+ tw-inset-0
28
+ "
29
+ :class="{
30
+ 'md:tw-px-2' : variant === 'lightbox',
31
+ 'tw-px-2' : variant === 'alert',
32
+ }"
33
+ >
34
+ <!-- the lightbox itself -->
35
+ <div
36
+ ref="kvLightbox"
37
+ tabindex="-1"
38
+ data-test="kv-lightbox"
39
+ class="
40
+ tw-bg-primary
41
+ tw-flex tw-flex-col
42
+ tw-mx-auto md:tw-my-auto
43
+
44
+ "
45
+ :class="{
46
+ 'tw-w-full md:tw-w-auto' : variant === 'lightbox',
47
+ 'tw-mt-auto md:tw-my-auto' : variant === 'lightbox',
48
+ 'tw-min-h-half-screen md:tw-min-h-0' : variant === 'lightbox',
49
+ 'tw-rounded-t md:tw-rounded' : variant === 'lightbox',
50
+ 'tw-my-auto tw-rounded' : variant === 'alert',
51
+ }"
52
+ style="max-width: 55.55rem; max-height: 90%"
53
+ aria-modal="true"
54
+ :aria-label="title ? title : null"
55
+ :aria-describedby="variant === 'alert' ? 'kvLightboxBody' : null"
56
+ :role="role"
57
+ @click.stop
58
+ >
59
+ <!-- header -->
60
+ <div
61
+ class="
62
+ tw-flex
63
+ tw-p-2.5 md:tw-px-4 md:tw-pt-4 md:tw-pb-3.5
64
+ "
65
+ >
66
+ <div class="tw-flex-grow">
67
+ <!-- @slot header -->
68
+ <slot name="header">
69
+ <h2 class="tw-text-h3 tw-flex-1">
70
+ {{ title }}
71
+ </h2>
72
+ </slot>
73
+ </div>
74
+ <button
75
+ v-if="!preventClose"
76
+ class="
77
+ tw-grid tw-content-center tw-justify-center
78
+ tw-ml-auto
79
+ tw-w-6 tw-h-6 tw--m-2
80
+ hover:tw-text-action-highlight
81
+ "
82
+ @click.stop="hide('close-x')"
83
+ >
84
+ <kv-material-icon
85
+ class="tw-w-3 tw-h-3"
86
+ :icon="mdiClose"
87
+ />
88
+ <span class="tw-sr-only">Close</span>
89
+ </button>
90
+ </div>
91
+
92
+ <!-- body -->
93
+ <div
94
+ id="kvLightboxBody"
95
+ ref="kvLightboxBody"
96
+ class="
97
+ tw-flex-1
98
+ tw-px-2.5 md:tw-px-4
99
+ tw-pb-2.5 md:tw-pb-4
100
+ tw-overflow-auto
101
+ "
102
+ >
103
+ <!-- @slot default -->
104
+ <slot></slot>
105
+ </div>
106
+
107
+ <!-- controls -->
108
+ <div
109
+ v-if="$slots.controls"
110
+ ref="controlsRef"
111
+ class="
112
+ tw-flex-shrink-0 tw-flex tw-justify-end tw-gap-x-2.5
113
+ tw-p-2.5 md:tw-px-4 md:tw-pb-4 md:tw-pt-1
114
+ "
115
+ >
116
+ <!-- @slot controls -->
117
+ <slot name="controls"></slot>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </transition>
124
+ </template>
125
+
126
+ <script>
127
+ import {
128
+ ref,
129
+ toRefs,
130
+ computed,
131
+ nextTick,
132
+ watch,
133
+ onBeforeUnmount,
134
+ onMounted,
135
+ } from 'vue-demi';
136
+ import { mdiClose } from '@mdi/js';
137
+ import { useFocusTrap } from '@vueuse/integrations/useFocusTrap';
138
+ import { hideOthers as makePageInert } from 'aria-hidden';
139
+ import { lockScroll, unlockScroll } from '../utils/scrollLock';
140
+ import { lockPrintSingleEl, unlockPrintSingleEl } from '../utils/printing';
141
+
142
+ import KvMaterialIcon from './KvMaterialIcon.vue';
143
+
144
+ /**
145
+ * Alert or a lightbox
146
+ * Accessibility: https://www.w3.org/TR/wai-aria-practices-1.1/#dialog_modal
147
+ *
148
+ * - [x] Tab and Shift + Tab do not move focus outside the dialog
149
+ * - [x] focus is initially set on the first focusable element (close button).
150
+ * - [x] focus is returned to the element that opened the dialog on close
151
+ * - [x] role = dialog
152
+ * - [x] aria-label is set to its title.
153
+ * - [x] adds aria-hidden=true to all elements other than this dialog when open. - https://github.com/theKashey/vue-focus-lock/issues/16
154
+ * - [x] scrolling only scrolls the lightbox contents, not the page itself
155
+
156
+ * Alert dialog - https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_alertdialog_role
157
+ *
158
+ * - [x] focus moves to the first non-destructive control, rather than the close button
159
+ * - [x] role = alertDialog
160
+ * - [x] aria-describedby is set to the id of the dialog body
161
+ *
162
+ * Printing
163
+ *
164
+ * - [x] Only prints the contents of the lightbox when open
165
+ */
166
+
167
+ export default {
168
+ components: {
169
+ KvMaterialIcon,
170
+ },
171
+ props: {
172
+ /**
173
+ * Whether the dialog is open or not
174
+ * */
175
+ visible: {
176
+ type: Boolean,
177
+ default: false,
178
+ },
179
+ /**
180
+ * Appearance and role of the lightbox
181
+ * @values lightbox, alert
182
+ * */
183
+ variant: {
184
+ type: String,
185
+ default: 'lightbox',
186
+ validator(value) {
187
+ return ['lightbox', 'alert'].includes(value);
188
+ },
189
+ },
190
+ /**
191
+ * The title of the dialog which describes the dialog to screenreaders, and if no
192
+ * content is in the `header` slot, will be displayed at the top of the lightbox.
193
+ * */
194
+ title: {
195
+ type: String,
196
+ required: true,
197
+ },
198
+ /**
199
+ * The dialog has no close X button, clicking the screen does not close,
200
+ * pressing ESC does not close.
201
+ * */
202
+ preventClose: {
203
+ type: Boolean,
204
+ default: false,
205
+ },
206
+ },
207
+ emits: [
208
+ 'lightbox-closed',
209
+ ],
210
+ setup(props, { emit }) {
211
+ const {
212
+ visible,
213
+ variant,
214
+ preventClose,
215
+ } = toRefs(props);
216
+
217
+ const kvLightbox = ref(null);
218
+ const kvLightboxBody = ref(null);
219
+ const controlsRef = ref(null);
220
+ const activateFocusTrap = ref(null);
221
+ const deactivateFocusTrap = ref(null);
222
+
223
+ // Ensure the lightbox ref isn't null
224
+ nextTick(() => {
225
+ const {
226
+ activate,
227
+ deactivate,
228
+ } = useFocusTrap([
229
+ kvLightbox.value, // This lightbox
230
+ '[role="alert"]', // Any open toasts/alerts on the page
231
+ ], {
232
+ allowOutsideClick: true, // allow clicking outside the lightbox to close it
233
+ });
234
+ activateFocusTrap.value = activate;
235
+ deactivateFocusTrap.value = deactivate;
236
+ });
237
+
238
+ let makePageInertCallback = null;
239
+ let onKeyUp = null;
240
+
241
+ const role = computed(() => {
242
+ if (variant.value === 'alert') {
243
+ return 'alertdialog';
244
+ }
245
+ return 'dialog';
246
+ });
247
+
248
+ const hide = (closedBy = '') => {
249
+ // scroll any content inside the lightbox back to top
250
+ if (kvLightbox.value && kvLightboxBody.value) {
251
+ deactivateFocusTrap.value?.();
252
+ kvLightboxBody.value.scrollTop = 0;
253
+ unlockPrintSingleEl(kvLightboxBody.value);
254
+ }
255
+ unlockScroll();
256
+ if (makePageInertCallback) {
257
+ makePageInertCallback();
258
+ makePageInertCallback = null;
259
+ }
260
+ document.removeEventListener('keyup', onKeyUp);
261
+
262
+ /**
263
+ * Triggered when the lightbox is closed
264
+ * @event lightbox-closed
265
+ * @type {Event}
266
+ */
267
+ emit('lightbox-closed', { type: closedBy });
268
+ };
269
+
270
+ onKeyUp = (e) => {
271
+ if (!!e && e.key === 'Escape' && !preventClose.value) {
272
+ hide();
273
+ }
274
+ };
275
+
276
+ const onScreenClick = () => {
277
+ if (!preventClose.value) {
278
+ hide('background-click');
279
+ }
280
+ };
281
+
282
+ const show = () => {
283
+ if (visible.value) {
284
+ document.addEventListener('keyup', onKeyUp);
285
+
286
+ nextTick(() => {
287
+ if (kvLightbox.value && kvLightboxBody.value) {
288
+ activateFocusTrap.value?.();
289
+ makePageInertCallback = makePageInert(kvLightbox.value);
290
+ lockPrintSingleEl(kvLightboxBody.value);
291
+ }
292
+ lockScroll();
293
+
294
+ // alerts should send focus to the first actionable item in the controls
295
+ if (variant.value === 'alert') {
296
+ const firstControlEl = controlsRef.value.querySelector('button');
297
+ if (firstControlEl) {
298
+ firstControlEl.focus();
299
+ }
300
+ }
301
+ });
302
+ }
303
+ };
304
+
305
+ watch(visible, () => {
306
+ if (visible.value) {
307
+ show();
308
+ } else {
309
+ hide();
310
+ }
311
+ });
312
+
313
+ onMounted(() => {
314
+ if (visible.value) {
315
+ show();
316
+ }
317
+ });
318
+
319
+ onBeforeUnmount(() => hide());
320
+
321
+ return {
322
+ mdiClose,
323
+ role,
324
+ kvLightbox,
325
+ kvLightboxBody,
326
+ onKeyUp,
327
+ onScreenClick,
328
+ hide,
329
+ show,
330
+ controlsRef,
331
+ };
332
+ },
333
+ };
334
+ </script>
@@ -0,0 +1,128 @@
1
+ <template>
2
+ <div class="tw-h-full tw-w-full tw-p-2.5">
3
+ <figure
4
+ class="tw-w-full tw-relative"
5
+ :style="{ height: graphHeight }"
6
+ >
7
+ <div
8
+ class="tw-w-full tw-h-full tw-bg-marigold-2 tw-opacity-low"
9
+ :style="{ clipPath: `polygon(${shade}, 100% 100%, 0% 100%)` }"
10
+ ></div>
11
+ <div
12
+ class="tw-absolute tw-top-0 tw-w-full tw-h-full tw-bg-marigold-2"
13
+ :style="{ clipPath: `polygon(${line})` }"
14
+ ></div>
15
+ <span
16
+ v-for="point in normalizedPoints"
17
+ :key="point.x"
18
+ class="
19
+ tw-absolute
20
+ tw-w-2
21
+ tw-h-2
22
+ tw-border
23
+ tw-border-white
24
+ tw-bg-marigold-2
25
+ tw-rounded-full
26
+ "
27
+ :style="{ left: `${point.x}%`, top: `${point.y}%`, transform: 'translate(-50%, -50%)' }"
28
+ ></span>
29
+ <template v-for="point in normalizedPoints">
30
+ <span
31
+ v-if="point.label"
32
+ :key="point.label"
33
+ class="tw-absolute"
34
+ :style="{ left: `${point.x}%`, bottom: '-3rem', transform: 'translate(-50%, -50%)' }"
35
+ >
36
+ {{ point.label }}
37
+ </span>
38
+ </template>
39
+ </figure>
40
+ <h4
41
+ v-if="axisLabel"
42
+ class="tw-text-center"
43
+ :class="{ 'tw-pt-1': !hasValueLabels, 'tw-pt-6': hasValueLabels }"
44
+ >
45
+ {{ axisLabel }}
46
+ </h4>
47
+ </div>
48
+ </template>
49
+
50
+ <script>
51
+ import { computed, toRefs } from 'vue-demi';
52
+
53
+ export default {
54
+ props: {
55
+ /**
56
+ * Array of objects like [{ value: 10, label: '2014' }, { value: 20, label: '2015' }]
57
+ */
58
+ points: {
59
+ type: Array,
60
+ required: true,
61
+ },
62
+ /**
63
+ * The optional label to show below the graph on the x-axis
64
+ */
65
+ axisLabel: {
66
+ type: String,
67
+ default: '',
68
+ },
69
+ },
70
+ setup(props) {
71
+ const { points, axisLabel } = toRefs(props);
72
+
73
+ // Get step to use on x-axis
74
+ const xIncrement = Math.round(100 / (points.value.length - 1));
75
+
76
+ // Find the largest value to be used as the scale of the graph
77
+ const largestY = computed(() => {
78
+ return points.value.reduce((prev, current) => {
79
+ return prev > current.value ? prev : current.value;
80
+ }, 0);
81
+ });
82
+
83
+ // Find the largest value to be used as the scale of the graph
84
+ const hasValueLabels = computed(() => {
85
+ return points.value.reduce((prev, current) => {
86
+ return prev || !!current.label;
87
+ }, false);
88
+ });
89
+
90
+ // Convert single values to points using increment and largest value
91
+ const normalizedPoints = computed(() => {
92
+ return points.value.reduce((prev, next, i) => {
93
+ prev.push({
94
+ x: i * xIncrement,
95
+ y: 100 - ((next.value / largestY.value) * 100),
96
+ label: next.label,
97
+ });
98
+
99
+ return prev;
100
+ }, []);
101
+ });
102
+
103
+ // Used for drawing the shading under the line
104
+ const shade = computed(() => (normalizedPoints.value.map(({ x, y }) => `${x}% ${y}%`).join(',')));
105
+
106
+ // Used for drawing the line
107
+ const line = computed(() => {
108
+ const topLine = normalizedPoints.value.map(({ x, y }) => `${x}% ${y + 0.3}%`).join(',');
109
+ const bottomLine = [...normalizedPoints.value].reverse().map(({ x, y }) => `${x}% ${y - 0.3}%`).join(',');
110
+ return `${topLine}, ${bottomLine}`;
111
+ });
112
+
113
+ const graphHeight = computed(() => {
114
+ const labelSpace = (axisLabel.value ? 2 : 0) + (hasValueLabels.value ? 2 : 0);
115
+
116
+ return `calc(100% - ${labelSpace}rem)`;
117
+ });
118
+
119
+ return {
120
+ hasValueLabels,
121
+ graphHeight,
122
+ normalizedPoints,
123
+ shade,
124
+ line,
125
+ };
126
+ },
127
+ };
128
+ </script>
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <div
3
+ class="
4
+ loading-placeholder
5
+ tw-w-full tw-h-full
6
+ tw-relative
7
+ tw-overflow-hidden
8
+ tw-bg-tertiary
9
+ tw-rounded-sm
10
+ "
11
+ >
12
+ </div>
13
+ </template>
14
+
15
+ <style scoped>
16
+ .loading-placeholder::before {
17
+ content: '';
18
+ display: block;
19
+ position: absolute;
20
+ height: 100%;
21
+ width: 100%;
22
+ top: 0;
23
+ transform: translateX(100%);
24
+ background:
25
+ linear-gradient(to right, transparent 0%, rgb(var(--bg-secondary)) 50%, transparent 100%);
26
+ animation: loading-placeholder 1.5s infinite cubic-bezier(0.4, 0, 0.2, 1);
27
+ }
28
+
29
+ @keyframes loading-placeholder {
30
+ from {
31
+ transform: translateX(-100%);
32
+ }
33
+
34
+ to {
35
+ transform: translateX(100%);
36
+ }
37
+ }
38
+ </style>
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <svg
3
+ class="tw-animate-spin-eased"
4
+ :class="{
5
+ 'tw-h-3 tw-w-3'
6
+ : size === 'small',
7
+ 'tw-h-4 tw-w-4'
8
+ : size === 'medium',
9
+ 'tw-h-8 tw-w-8'
10
+ : size === 'large',
11
+ 'tw-text-brand'
12
+ : color === 'brand',
13
+ 'tw-text-white'
14
+ : color === 'white',
15
+ 'tw-text-black'
16
+ : color === 'black',
17
+ }"
18
+ fill="none"
19
+ xmlns="http://www.w3.org/2000/svg"
20
+ viewBox="0 0 24 24"
21
+ >
22
+ <g
23
+ clip-path="url(#clip0)"
24
+ stroke-width="2"
25
+ >
26
+ <circle
27
+ class="tw-stroke-current tw-text-tertiary tw-opacity-low"
28
+ cx="12"
29
+ cy="12"
30
+ r="8"
31
+ transform="rotate(135 12 12)"
32
+ />
33
+ <path
34
+ d="M17.447 17.866c3.24-3.24 3.334-8.399.21-11.523-3.125-3.124-8.284-3.03-11.524.21"
35
+ stroke="currentColor"
36
+ stroke-linecap="round"
37
+ />
38
+ </g>
39
+ <defs>
40
+ <clipPath id="clip0">
41
+ <path
42
+ fill="#fff"
43
+ d="M0 0h24v24H0z"
44
+ />
45
+ </clipPath>
46
+ </defs>
47
+ </svg>
48
+ </template>
49
+
50
+ <script>
51
+ /**
52
+ * Animated SVG Loading Spinner with variable size and color.
53
+ */
54
+
55
+ export default {
56
+ props: {
57
+ /**
58
+ * The size of the loading spinner.
59
+ * 'small', 'medium', or 'large'
60
+ * */
61
+ size: {
62
+ type: String,
63
+ default: 'medium',
64
+ validator(value) {
65
+ return ['small', 'medium', 'large'].includes(value);
66
+ },
67
+ },
68
+ /**
69
+ * The color of the loading spinner.
70
+ * 'brand', 'white', or 'black'
71
+ * */
72
+ color: {
73
+ type: String,
74
+ default: 'brand',
75
+ validator(value) {
76
+ return ['brand', 'white', 'black'].includes(value);
77
+ },
78
+ },
79
+ },
80
+ };
81
+ </script>