@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,47 @@
1
+ <template>
2
+ <span
3
+ class="tw-inline-flex"
4
+ aria-hidden="true"
5
+ role="img"
6
+ >
7
+ <svg
8
+ class="tw-w-full tw-h-full tw-fill-current"
9
+ width="24"
10
+ height="24"
11
+ viewBox="0 0 24 24"
12
+ >
13
+ <path :d="icon" />
14
+ </svg>
15
+ </span>
16
+ </template>
17
+
18
+ <script>
19
+ /**
20
+ * This component takes path data as a prop from an imported @mdi/js module and
21
+ * renders the icon as an SVG.
22
+ *
23
+ * Find your icon name from https://materialdesignicons.com/,
24
+ * The module name to import will be 'mdiPascalCaseIconName'. `account-cowboy-hat` would be
25
+ * `import { mdiAccountCowboyHat } from '@mdi/js'`;
26
+ *
27
+ * These icons do not announce to screenreaders. If you are using a non-decorative icon, be sure
28
+ * to include screen reader text. E.g.,
29
+ * `<button>
30
+ * <kv-material-icon :icon="mdiClose" />
31
+ * <span class="sr-only">Close modal</span>
32
+ * </button>`
33
+ */
34
+
35
+ export default {
36
+ props: {
37
+ /**
38
+ * SVG path data passed in from an imported @mdi/js module.
39
+ * E.g., `import { mdiAccount } from '@mdi/js'; <kv-material-icon :icon="mdiAccount" />`
40
+ * */
41
+ icon: {
42
+ type: String,
43
+ default: '',
44
+ },
45
+ },
46
+ };
47
+ </script>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <div
3
+ class="tw-mx-auto tw-px-2.5 md:tw-px-4 lg:tw-px-8"
4
+ style="max-width: 1200px;"
5
+ >
6
+ <slot></slot>
7
+ </div>
8
+ </template>
9
+
10
+ <script>
11
+ /**
12
+ * Wrapper component which contains content to the specified widths and margins set by design
13
+ */
14
+ export default {};
15
+ </script>
@@ -0,0 +1,198 @@
1
+ <template>
2
+ <nav aria-label="Pagination">
3
+ <ul
4
+ class="tw-text-center tw-mx-auto tw-my-1.5 tw-flex tw-justify-between tw-items-center"
5
+ style="max-width: 17rem;"
6
+ >
7
+ <li>
8
+ <a
9
+ class="tw-cursor-pointer tw-flex"
10
+ :class="linkClass(0)"
11
+ aria-label="Previous page"
12
+ @click="!isCurrent(0) && clickPrevious()"
13
+ >
14
+ <kv-material-icon
15
+ :icon="mdiChevronLeft"
16
+ class="tw-h-4 tw-w-4"
17
+ />
18
+ <span class="tw-sr-only">Previous page</span>
19
+ </a>
20
+ </li>
21
+ <li
22
+ v-for="(n, i) in numbers"
23
+ :key="i"
24
+ :aria-hidden="isEllipsis(n)"
25
+ >
26
+ <template v-if="isEllipsis(n)">
27
+ ...
28
+ </template>
29
+ <a
30
+ v-else
31
+ class="tw-cursor-pointer"
32
+ :class="linkClass(n)"
33
+ :aria-label="`Page ${n + 1}`"
34
+ @click="!isCurrent(n) && clickPage(n)"
35
+ >
36
+ <span
37
+ v-if="isCurrent(n)"
38
+ class="tw-sr-only"
39
+ >You're on page</span>
40
+ {{ n + 1 }}
41
+ </a>
42
+ </li>
43
+ <li>
44
+ <a
45
+ class="tw-cursor-pointer tw-flex"
46
+ :class="linkClass(totalPages ? totalPages - 1 : 0)"
47
+ aria-label="Next page"
48
+ @click="totalPages && !isCurrent(totalPages - 1) && clickNext()"
49
+ >
50
+ <kv-material-icon
51
+ :icon="mdiChevronRight"
52
+ class="tw-h-4 tw-w-4"
53
+ />
54
+ <span class="tw-sr-only">Next page</span>
55
+ </a>
56
+ </li>
57
+ </ul>
58
+ </nav>
59
+ </template>
60
+
61
+ <script>
62
+ import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
63
+ import KvMaterialIcon from './KvMaterialIcon.vue';
64
+
65
+ export default {
66
+ name: 'KvPagination',
67
+ components: {
68
+ KvMaterialIcon,
69
+ },
70
+ props: {
71
+ limit: {
72
+ type: Number,
73
+ required: true,
74
+ validator: (value) => value > 0,
75
+ },
76
+ total: {
77
+ type: Number,
78
+ required: true,
79
+ validator: (value) => value >= 0,
80
+ },
81
+ offset: {
82
+ type: Number,
83
+ default: 0,
84
+ validator: (value) => value >= 0,
85
+ },
86
+ extraPages: {
87
+ type: Number,
88
+ default: 3,
89
+ validator: (value) => value > 0,
90
+ },
91
+ scrollToTop: {
92
+ type: Boolean,
93
+ default: true,
94
+ },
95
+ kvTrackFunction: {
96
+ type: Function,
97
+ default: () => {},
98
+ },
99
+ trackEventCategory: {
100
+ type: String,
101
+ default: '',
102
+ },
103
+ },
104
+ data() {
105
+ return {
106
+ mdiChevronLeft,
107
+ mdiChevronRight,
108
+ };
109
+ },
110
+ computed: {
111
+ current() {
112
+ const page = Math.ceil(this.offset / this.limit);
113
+
114
+ // This component uses a 0-based page index
115
+ return page < this.totalPages ? page : 0;
116
+ },
117
+ totalPages() {
118
+ return Math.ceil(this.total / this.limit);
119
+ },
120
+ numbers() {
121
+ // If less than the max, there will be no ellipsis, so just return the numbers
122
+ if (this.totalPages < (this.extraPages + 2)) {
123
+ return this.range(0, this.totalPages - 1);
124
+ }
125
+
126
+ const numbers = [];
127
+
128
+ // Add the 'middle' block of numbers based upon the current page
129
+ if ([0, 1, 2].includes(this.current)) {
130
+ numbers.push(...this.range(1, this.extraPages));
131
+ } else if ([this.totalPages - 3, this.totalPages - 2, this.totalPages - 1]
132
+ .includes(this.current)) {
133
+ numbers.push(...this.range(this.totalPages - this.extraPages - 1, this.totalPages - 2));
134
+ } else {
135
+ const delta = Math.floor(this.extraPages / 2);
136
+ numbers.push(...this.range(this.current - delta, this.current + delta));
137
+ }
138
+
139
+ // Add a placeholder for first ellipsis
140
+ if (numbers[1] !== 2) {
141
+ numbers.splice(0, 0, -1);
142
+ }
143
+
144
+ // Add a placeholder for second ellipsis
145
+ const totalNumbers = numbers.length;
146
+ if (numbers[totalNumbers - 1] !== this.totalPages - 2) {
147
+ numbers.splice(totalNumbers, 0, -1);
148
+ }
149
+
150
+ // Add first and last pages
151
+ numbers.unshift(0);
152
+ numbers.push(this.totalPages - 1);
153
+
154
+ return numbers;
155
+ },
156
+ },
157
+ methods: {
158
+ range(start, end) {
159
+ return [...Array(end - start + 1)].map((_, n) => n + start);
160
+ },
161
+ isCurrent(number) {
162
+ return number === this.current;
163
+ },
164
+ isEllipsis(number) {
165
+ return number === -1;
166
+ },
167
+ linkClass(number) {
168
+ return { 'tw-text-tertiary': this.isCurrent(number), 'tw-pointer-events-none': this.isCurrent(number) };
169
+ },
170
+ pageChange(number) {
171
+ if (this.scrollToTop && window.scrollTo) {
172
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
173
+ }
174
+
175
+ this.$emit('page-changed', { pageOffset: number * this.limit });
176
+ },
177
+ clickPage(number) {
178
+ this.pageChange(number);
179
+
180
+ this.kvTrackFunction(this.trackEventCategory, 'click', 'pagination-number', null, number + 1);
181
+ },
182
+ clickPrevious() {
183
+ const previous = this.current - 1;
184
+
185
+ this.pageChange(previous);
186
+
187
+ this.kvTrackFunction(this.trackEventCategory, 'click', 'pagination-previous', null, previous + 1);
188
+ },
189
+ clickNext() {
190
+ const next = this.current + 1;
191
+
192
+ this.pageChange(next);
193
+
194
+ this.kvTrackFunction(this.trackEventCategory, 'click', 'pagination-next', null, next + 1);
195
+ },
196
+ },
197
+ };
198
+ </script>
@@ -0,0 +1,257 @@
1
+ <template>
2
+ <figure
3
+ class="pie-chart tw-flex tw-flex-col md:tw-flex-row tw-items-center tw-justify-center tw-gap-2"
4
+ @mouseleave="activeSlice = null"
5
+ >
6
+ <!-- pie chart -->
7
+ <div class="tw-relative">
8
+ <div
9
+ v-if="loading"
10
+ class="pie-placeholder tw-mt-2.5"
11
+ >
12
+ <div class="tw-overflow-hidden tw-rounded-full">
13
+ <kv-loading-placeholder />
14
+ </div>
15
+ </div>
16
+ <svg
17
+ v-else
18
+ viewBox="0 0 32 32"
19
+ xmlns="http://www.w3.org/2000/svg"
20
+ xmlns:xlink="http://www.w3.org/1999/xlink"
21
+ >
22
+ <path
23
+ v-for="(slice, index) in slices"
24
+ :key="index"
25
+ class="tw-origin-center tw-transition-transform"
26
+ :style="isSliceActive(slice) ? { transform: 'scale(1.1)' } : {}"
27
+ :d="slice.path"
28
+ :stroke="slice.color"
29
+ :stroke-width="lineWidth"
30
+ fill="none"
31
+ @mouseenter="setActiveSlice(slice)"
32
+ @click="setActiveSlice(slice)"
33
+ />
34
+ </svg>
35
+ <!-- active slice -->
36
+ <div
37
+ v-if="activeSlice"
38
+ class="
39
+ tw-absolute tw-top-1/2 tw-left-1/2 -tw-translate-x-1/2 -tw-translate-y-1/2
40
+ tw-flex tw-flex-col tw-items-center tw-text-center"
41
+ style="max-width: 8.5rem;"
42
+ >
43
+ <div class="tw-font-medium tw-line-clamp-4">
44
+ {{ activeSlice.label }}
45
+ </div>
46
+ <div>
47
+ {{ activeSlice.value }} ({{ activeSlicePercent }})
48
+ </div>
49
+ </div>
50
+ </div>
51
+ <!-- key -->
52
+ <div
53
+ style="width: 14rem; height: 85%;"
54
+ class="tw-flex tw-flex-col tw-justify-between"
55
+ >
56
+ <ol class="tw-pl-4 md:tw-pl-0">
57
+ <li
58
+ v-for="(slice, index) in slices.slice(pageIndex * slicesPerPage, (pageIndex + 1) * slicesPerPage)"
59
+ :key="index"
60
+ class="tw-flex tw-items-center"
61
+ @mouseenter="setActiveSlice(slice)"
62
+ @click="setActiveSlice(slice)"
63
+ >
64
+ <div
65
+ class="tw-w-2 tw-h-2 tw-mr-1 tw-rounded-full tw-flex-none"
66
+ :style="{ backgroundColor: slice.color }"
67
+ ></div>
68
+ <span class="tw-truncate">
69
+ {{ slice.label }}
70
+ </span>
71
+ </li>
72
+ </ol>
73
+ <!-- paging controls -->
74
+ <div
75
+ v-if="pageCount > 1"
76
+ class="tw-flex tw-justify-center md:tw-justify-start"
77
+ >
78
+ <button
79
+ :disabled="pageIndex === 0"
80
+ class="tw-font-medium tw-p-0.5 disabled:tw-opacity-low"
81
+ @click="prevPage"
82
+ >
83
+ &lt;
84
+ </button>
85
+ {{ pageIndex + 1 }} / {{ pageCount }}
86
+ <button
87
+ :disabled="pageIndex === pageCount - 1"
88
+ class="tw-font-medium tw-p-0.5 disabled:tw-opacity-low"
89
+ @click="nextPage"
90
+ >
91
+ &gt;
92
+ </button>
93
+ </div>
94
+ </div>
95
+ </figure>
96
+ </template>
97
+
98
+ <script>
99
+ import numeral from 'numeral';
100
+ import {
101
+ ref,
102
+ toRaw,
103
+ toRefs,
104
+ computed,
105
+ } from 'vue-demi';
106
+ import Alea from '../utils/Alea';
107
+ import KvLoadingPlaceholder from './KvLoadingPlaceholder.vue';
108
+
109
+ // convenience function to get point on circumference of a given circle (from https://codepen.io/grieve/pen/xwGMJp)
110
+ function circumPointFromAngle(cx, cy, r, a) {
111
+ return [
112
+ cx + r * Math.cos(a),
113
+ cy + r * Math.sin(a),
114
+ ];
115
+ }
116
+
117
+ export default {
118
+ name: 'PieChartFigure',
119
+ components: {
120
+ KvLoadingPlaceholder,
121
+ },
122
+ props: {
123
+ loading: {
124
+ type: Boolean,
125
+ default: true,
126
+ },
127
+ values: {
128
+ type: Array,
129
+ default: () => ([]),
130
+ },
131
+ },
132
+ emits: [
133
+ 'click',
134
+ ],
135
+ setup(props, { emit }) {
136
+ const {
137
+ values,
138
+ } = toRefs(props);
139
+
140
+ const svgSize = ref(32);
141
+ const lineWidth = ref(5);
142
+ const activeSlice = ref(null);
143
+ const pageIndex = ref(0);
144
+ const slicesPerPage = ref(10);
145
+
146
+ const activeSlicePercent = computed(() => {
147
+ return activeSlice.value ? numeral(activeSlice.value.percent).format('0.00%') : '';
148
+ });
149
+
150
+ const radius = computed(() => {
151
+ return (svgSize.value / 2) - (lineWidth.value / 2) - 2;
152
+ });
153
+
154
+ const center = computed(() => {
155
+ return svgSize.value / 2;
156
+ });
157
+
158
+ const pickColor = (index) => {
159
+ const rng = new Alea('loans', index, 'kiva');
160
+ let color = '#';
161
+ for (let i = 0; i < 3; i += 1) {
162
+ color += Math.floor(rng() * 256).toString(16).padStart(2, '0');
163
+ }
164
+ return color;
165
+ };
166
+
167
+ const slices = computed(() => {
168
+ const slicesArr = [];
169
+ // center point
170
+ const cX = center.value;
171
+ const cY = cX;
172
+ // radius
173
+ const r = radius.value;
174
+ // starting angle
175
+ let start = -0.25;
176
+ // loop through each value and create a pie slice path
177
+ for (let i = 0; i < values.value.length; i += 1) {
178
+ const value = values.value[i];
179
+ const end = start + value.percent;
180
+ const [startX, startY] = circumPointFromAngle(cX, cY, r, start * Math.PI * 2);
181
+ let path = `M ${startX},${startY} `;
182
+ if (value.percent > 0.99) {
183
+ // Draw a full circle in two arcs
184
+ const [midX, midY] = circumPointFromAngle(cX, cY, r, (start + end) * Math.PI);
185
+ path += `A ${r},${r} 0 0,1 ${midX},${midY} `;
186
+ path += `A ${r},${r} 0 0,1 ${startX},${startY}`;
187
+ } else {
188
+ // Draw just the outer arc of the slice
189
+ const [endX, endY] = circumPointFromAngle(cX, cY, r, end * Math.PI * 2);
190
+ const largeArc = value.percent > 0.5 ? 1 : 0;
191
+ path += `A ${r},${r} 0 ${largeArc},1 ${endX},${endY}`;
192
+ }
193
+ slicesArr.push({
194
+ ...value,
195
+ path,
196
+ color: pickColor(i),
197
+ });
198
+ start = end;
199
+ }
200
+ return slicesArr;
201
+ });
202
+
203
+ const pageCount = computed(() => {
204
+ return Math.ceil(slices.value.length / slicesPerPage.value);
205
+ });
206
+
207
+ const prevPage = () => {
208
+ if (pageIndex.value > 0) {
209
+ pageIndex.value -= 1;
210
+ }
211
+ };
212
+ const nextPage = () => {
213
+ if (pageIndex.value < pageCount.value - 1) {
214
+ pageIndex.value += 1;
215
+ }
216
+ };
217
+
218
+ const isSliceActive = (slice) => {
219
+ return toRaw(activeSlice.value) === slice;
220
+ };
221
+
222
+ const setActiveSlice = (slice) => {
223
+ activeSlice.value = slice;
224
+ emit('click', slice.label);
225
+ };
226
+
227
+ return {
228
+ svgSize,
229
+ lineWidth,
230
+ activeSlice,
231
+ pageIndex,
232
+ slicesPerPage,
233
+ activeSlicePercent,
234
+ radius,
235
+ center,
236
+ slices,
237
+ pageCount,
238
+ prevPage,
239
+ nextPage,
240
+ isSliceActive,
241
+ setActiveSlice,
242
+ };
243
+ },
244
+ };
245
+ </script>
246
+
247
+ <style lang="postcss" scoped>
248
+ .pie-chart svg {
249
+ width: 20rem;
250
+ height: 20rem;
251
+ }
252
+
253
+ .pie-placeholder, .pie-placeholder div {
254
+ width: 17.5rem;
255
+ height: 17.5rem;
256
+ }
257
+ </style>
@@ -0,0 +1,178 @@
1
+ <template>
2
+ <transition :name="transitionType">
3
+ <div
4
+ v-show="show"
5
+ class="tw-absolute"
6
+ :style="styles"
7
+ :aria-hidden="show ? 'false' : 'true'"
8
+ >
9
+ <slot></slot>
10
+ </div>
11
+ </transition>
12
+ </template>
13
+
14
+ <script>
15
+ import {
16
+ onBodyTouchstart,
17
+ offBodyTouchstart,
18
+ isTargetElement,
19
+ } from '../utils/touchEvents';
20
+
21
+ export default {
22
+ name: 'KvPopper',
23
+ props: {
24
+ controller: {
25
+ validator(value) {
26
+ if (typeof value === 'string') return true;
27
+ if (typeof window === 'object'
28
+ && 'HTMLElement' in window
29
+ && value instanceof HTMLElement) return true;
30
+ return false;
31
+ },
32
+ required: true,
33
+ },
34
+ openDelay: { type: Number, default: 0 },
35
+ closeDelay: { type: Number, default: 200 },
36
+ // must be defined in our globa/transitions.scss
37
+ transitionType: { type: String, default: '' },
38
+ popperPlacement: { type: String, default: 'bottom-start' },
39
+ popperModifiers: { type: Object, default: () => {} },
40
+ },
41
+ data() {
42
+ return {
43
+ popper: null,
44
+ popperPromise: null,
45
+ styles: {},
46
+ show: false,
47
+ timeout: null,
48
+ };
49
+ },
50
+ computed: {
51
+ reference() {
52
+ return typeof this.controller === 'string' ? document.getElementById(this.controller) : this.controller;
53
+ },
54
+ },
55
+ watch: {
56
+ show(showing) {
57
+ if (this.reference) {
58
+ this.reference.setAttribute('aria-expanded', showing ? 'true' : 'false');
59
+ }
60
+ this.$emit(showing ? 'show' : 'hide');
61
+ },
62
+ },
63
+ mounted() {
64
+ this.reference.tabIndex = 0;
65
+ this.attachEvents();
66
+ },
67
+ updated() {
68
+ if (this.popper) {
69
+ this.popper.scheduleUpdate();
70
+ }
71
+ },
72
+ beforeDestroy() {
73
+ this.removeEvents();
74
+ if (this.popper) {
75
+ this.popper.destroy();
76
+ }
77
+ },
78
+ methods: {
79
+ open() {
80
+ this.initPopper().then(() => {
81
+ this.setTimeout(() => {
82
+ this.show = true;
83
+ this.popper.scheduleUpdate();
84
+ this.attachBodyEvents();
85
+ }, this.openDelay);
86
+ });
87
+ },
88
+ close() {
89
+ this.setTimeout(() => {
90
+ this.show = false;
91
+ this.removeBodyEvents();
92
+ }, this.closeDelay);
93
+ },
94
+ toggle() {
95
+ if (this.show) {
96
+ this.close();
97
+ } else {
98
+ this.open();
99
+ }
100
+ },
101
+ update() {
102
+ if (this.popper) {
103
+ this.popper.scheduleUpdate();
104
+ }
105
+ },
106
+ initPopper() {
107
+ // skip loading if popper already created
108
+ if (this.popper) return Promise.resolve();
109
+ // skip loading if loading already started
110
+ if (this.popperPromise) return this.popperPromise;
111
+ // import and init Popper.js
112
+ this.popperPromise = import('popper.js').then(({ default: Popper }) => {
113
+ this.popper = new Popper(this.reference, this.$el, {
114
+ placement: this.popperPlacement,
115
+ modifiers: {
116
+ applyStyle: (data) => {
117
+ this.styles = data.styles;
118
+ this.setAttributes(data.attributes);
119
+ },
120
+ preventOverflow: {
121
+ padding: 0,
122
+ },
123
+ ...this.popperModifiers,
124
+ },
125
+ });
126
+ });
127
+ return this.popperPromise;
128
+ },
129
+ bodyTouchHandler(e) {
130
+ if (!isTargetElement(e, [this.reference, this.$el])) {
131
+ this.show = false;
132
+ this.removeBodyEvents();
133
+ }
134
+ },
135
+ referenceTapHandler(e) {
136
+ e.preventDefault();
137
+ this.toggle();
138
+ },
139
+ attachEvents() {
140
+ this.reference.addEventListener('mouseover', this.open);
141
+ this.reference.addEventListener('focus', this.open);
142
+ this.reference.addEventListener('mouseout', this.close);
143
+ this.reference.addEventListener('blur', this.close);
144
+ this.$el.addEventListener('mouseover', this.open);
145
+ this.$el.addEventListener('mouseout', this.close);
146
+ this.reference.addEventListener('touchstart', this.referenceTapHandler);
147
+ },
148
+ attachBodyEvents() {
149
+ onBodyTouchstart(this.bodyTouchHandler);
150
+ },
151
+ removeEvents() {
152
+ this.removeBodyEvents();
153
+ this.reference.removeEventListener('touchstart', this.referenceTapHandler);
154
+ this.reference.removeEventListener('mouseover', this.open);
155
+ this.reference.removeEventListener('mouseout', this.close);
156
+ this.$el.removeEventListener('mouseover', this.open);
157
+ this.$el.removeEventListener('mouseout', this.close);
158
+ },
159
+ removeBodyEvents() {
160
+ offBodyTouchstart(this.bodyTouchHandler);
161
+ },
162
+ setAttributes(attrs) {
163
+ Object.keys(attrs).forEach((attr) => {
164
+ const value = attrs[attr];
165
+ if (value === false) {
166
+ this.$el.removeAttribute(attr);
167
+ } else {
168
+ this.$el.setAttribute(attr, value);
169
+ }
170
+ });
171
+ },
172
+ setTimeout(fn, delay) {
173
+ window.clearTimeout(this.timeout);
174
+ this.timeout = window.setTimeout(fn, delay);
175
+ },
176
+ },
177
+ };
178
+ </script>