@kiva/kv-components 10.6.1 → 10.8.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.
@@ -0,0 +1,5 @@
1
+ export declare const READ_MORE_CLASS = "kv-loan-use-read-more";
2
+ export declare const overflows: (statement: HTMLElement) => boolean;
3
+ export declare const findStatementTail: (statement: HTMLElement) => ChildNode;
4
+ export declare const trimStatementToFit: (statement: HTMLElement) => void;
5
+ export declare const fitStatement: (statement: HTMLElement, html: string) => void;
@@ -0,0 +1,40 @@
1
+ const S = "kv-loan-use-read-more";
2
+ const l = (e) => e.scrollHeight > e.clientHeight + 1, a = (e) => {
3
+ var t;
4
+ return e.nodeType === Node.TEXT_NODE && !!((t = e.textContent) != null && t.trim());
5
+ }, u = (e) => {
6
+ const t = Array.from(e.childNodes), o = e.querySelector(`.${S}`), n = o ? t.indexOf(o) : t.length;
7
+ return t.slice(0, n).reverse().find(a) ?? null;
8
+ }, h = (e) => {
9
+ const t = u(e);
10
+ if (!t) return;
11
+ const o = t.textContent ?? "", n = /^\s/.test(o) ? " " : "", s = o.trim().split(/\s+/), d = (r) => {
12
+ const x = s.slice(0, r).join(" ").replace(/[,;:]$/, "");
13
+ t.textContent = `${n}${x}… `;
14
+ };
15
+ let i = 1, c = s.length - 1, f = 1;
16
+ for (; i <= c; ) {
17
+ const r = Math.floor((i + c) / 2);
18
+ d(r), l(e) ? c = r - 1 : (f = r, i = r + 1);
19
+ }
20
+ d(f);
21
+ }, p = (e, t) => {
22
+ if (e.innerHTML = t, !l(e)) return;
23
+ const o = e.querySelector(`.${S}`);
24
+ if (o) {
25
+ if (o.remove(), !l(e)) {
26
+ const n = u(e);
27
+ n && (n.textContent = (n.textContent ?? "").trimEnd());
28
+ return;
29
+ }
30
+ e.innerHTML = t;
31
+ }
32
+ h(e);
33
+ };
34
+ export {
35
+ S as READ_MORE_CLASS,
36
+ u as findStatementTail,
37
+ p as fitStatement,
38
+ l as overflows,
39
+ h as trimStatementToFit
40
+ };
@@ -1 +1 @@
1
- .loan-card-use[data-v-330ff302]:hover,.loan-card-use[data-v-330ff302]:focus{--tw-text-opacity: 1;color:rgba(var(--text-primary),var(--tw-text-opacity, 1))}.loan-card-active-hover:hover .loan-card-use[data-v-330ff302]{text-decoration-line:underline}.loan-card-progress[data-v-330ff302]:hover,.loan-card-progress[data-v-330ff302]:focus{text-decoration-line:none}.loan-card-use-text-light-detailed[data-v-330ff302] p{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3}.loan-card-cta[data-v-330ff302] form,.loan-card-cta[data-v-330ff302] fieldset,.loan-card-cta[data-v-330ff302] .tw-w-full,.loan-card-cta[data-v-330ff302] button{display:flex!important;height:2.5rem!important;max-height:2.5rem!important;align-items:center!important;justify-content:center!important}.loan-card-cta[data-v-330ff302] .tw-inline-flex{height:2.5rem!important;max-height:2.5rem!important;min-height:2.5rem!important}[data-v-330ff302] .amount-dropdown select{height:2.25rem;--tw-border-opacity: 1;border-color:rgb(245 245 245 / var(--tw-border-opacity, 1));padding-left:.5rem}[data-v-330ff302] .goal-variant-callouts span{font-weight:500}.goal-variant-progress-group[data-v-330ff302] div[role=progressbar]{background-color:#d9d9d9}.light-detailed-progress-group[data-v-330ff302] p:first-of-type{margin-bottom:.5rem}
1
+ .loan-card-use[data-v-6313c6b8]:hover,.loan-card-use[data-v-6313c6b8]:focus{--tw-text-opacity: 1;color:rgba(var(--text-primary),var(--tw-text-opacity, 1))}.loan-card-active-hover:hover .loan-card-use[data-v-6313c6b8]{text-decoration-line:underline}.loan-card-progress[data-v-6313c6b8]:hover,.loan-card-progress[data-v-6313c6b8]:focus{text-decoration-line:none}.loan-card-use-text-light-detailed[data-v-6313c6b8] p{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3}.loan-card-cta[data-v-6313c6b8] form,.loan-card-cta[data-v-6313c6b8] fieldset,.loan-card-cta[data-v-6313c6b8] .tw-w-full,.loan-card-cta[data-v-6313c6b8] button{display:flex!important;height:2.5rem!important;max-height:2.5rem!important;align-items:center!important;justify-content:center!important}.loan-card-cta[data-v-6313c6b8] .tw-inline-flex{height:2.5rem!important;max-height:2.5rem!important;min-height:2.5rem!important}[data-v-6313c6b8] .amount-dropdown select{height:2.25rem;--tw-border-opacity: 1;border-color:rgb(245 245 245 / var(--tw-border-opacity, 1));padding-left:.5rem}[data-v-6313c6b8] .goal-variant-callouts span{font-weight:500}.goal-variant-progress-group[data-v-6313c6b8] div[role=progressbar]{background-color:#d9d9d9}.light-detailed-progress-group[data-v-6313c6b8] p:first-of-type{margin-bottom:.5rem}
@@ -1,6 +1,6 @@
1
1
  import D from "./KvCompactLoanCard.script.js";
2
2
  import { KV_COMPACT_LOAN_CARD_FRAGMENT as gt, KV_COMPACT_LOAN_CARD_USER_FRAGMENT as ft } from "./KvCompactLoanCard.script.js";
3
- import { resolveComponent as h, openBlock as a, createElementBlock as l, normalizeClass as c, createVNode as i, createCommentVNode as g, createElementVNode as r, normalizeStyle as _, createBlock as d, resolveDynamicComponent as k, withCtx as f, Fragment as s, toDisplayString as w, withModifiers as R, renderList as u } from "vue";
3
+ import { resolveComponent as h, openBlock as a, createElementBlock as l, normalizeClass as c, createVNode as i, createCommentVNode as g, createElementVNode as r, normalizeStyle as x, createBlock as d, resolveDynamicComponent as k, withCtx as f, Fragment as s, toDisplayString as w, withModifiers as R, renderList as u } from "vue";
4
4
  import "./KvCompactLoanCard.css";
5
5
  import B from "../_virtual/_plugin-vue_export-helper.js";
6
6
  const S = ["aria-busy"], G = { class: "tw-flex tw-flex-col tw-items-start tw-w-full" }, U = {
@@ -32,7 +32,7 @@ const S = ["aria-busy"], G = { class: "tw-flex tw-flex-col tw-items-start tw-w-f
32
32
  class: "tw-flex-1 tw-min-w-0",
33
33
  style: { "max-width": "6.5rem" }
34
34
  }, at = ["value"], ot = { class: "tw-flex-1 tw-min-w-0" }, nt = { class: "tw-p-1 tw-rounded-l-sm tw-border tw-border-gray-100 tw-border-r-0" };
35
- function lt(b, n, e, t, rt, x) {
35
+ function lt(b, n, e, t, rt, _) {
36
36
  var V;
37
37
  const v = h("kv-material-icon"), m = h("kv-loading-placeholder"), P = h("kv-borrower-image"), C = h("kv-loan-tag"), L = h("kv-loan-use"), M = h("kv-loan-callouts"), y = h("kv-loan-progress-group"), N = h("kv-select"), A = h("kv-lend-cta");
38
38
  return a(), l("div", {
@@ -60,7 +60,7 @@ function lt(b, n, e, t, rt, x) {
60
60
  "!tw-gap-1": e.showLightView || t.isLightDetailedVariant,
61
61
  "!tw-gap-1.5 md:!tw-gap-2.5": t.isPostGoalVariant
62
62
  }]),
63
- style: _(t.customStyle)
63
+ style: x(t.customStyle)
64
64
  }, [
65
65
  t.isLoading ? (a(), d(m, {
66
66
  key: 0,
@@ -70,7 +70,7 @@ function lt(b, n, e, t, rt, x) {
70
70
  "!tw-w-7.5 !tw-h-7.5 md:!tw-w-12.5 md:!tw-h-12.5": t.isPostGoalVariant,
71
71
  "!tw-w-9.5 !tw-h-9.5": t.isLightDetailedVariant
72
72
  }),
73
- style: _(t.borrowerImageStyle)
73
+ style: x(t.borrowerImageStyle)
74
74
  }, null, 8, ["class", "style"])) : (a(), l("div", U, [
75
75
  (a(), d(k(t.tag), {
76
76
  to: t.readMorePath,
@@ -88,7 +88,7 @@ function lt(b, n, e, t, rt, x) {
88
88
  "!tw-w-7.5 !tw-h-7.5 md:!tw-w-12.5 md:!tw-h-12.5": t.isPostGoalVariant,
89
89
  "!tw-w-9.5 !tw-h-9.5": t.isLightDetailedVariant
90
90
  }]),
91
- style: _(t.borrowerImageStyle),
91
+ style: x(t.borrowerImageStyle),
92
92
  alt: `Photo of ${t.borrowerName}`,
93
93
  "aspect-ratio": 1,
94
94
  "default-image": { width: 200, faceZoom: 50 },
@@ -234,7 +234,7 @@ function lt(b, n, e, t, rt, x) {
234
234
  ]))), 128))
235
235
  ])) : (a(), l("div", {
236
236
  key: 1,
237
- class: c(["tw-w-full tw-overflow-hidden", { "!tw--mt-1": e.showTags && x.hasMatchingInfo && e.showLightView }])
237
+ class: c(["tw-w-full tw-overflow-hidden", { "!tw--mt-1": e.showTags && _.hasMatchingInfo && e.showLightView }])
238
238
  }, [
239
239
  i(L, {
240
240
  use: t.loanUse,
@@ -247,8 +247,9 @@ function lt(b, n, e, t, rt, x) {
247
247
  country: t.formattedLocation,
248
248
  "show-read-more": e.showLightView,
249
249
  "truncate-words-number": e.truncateWordsNumber,
250
+ "max-lines": e.loanUseMaxLines,
250
251
  class: "tw-text-small tw-leading-tight"
251
- }, null, 8, ["use", "loan-amount", "status", "borrower-count", "name", "distribution-model", "country", "show-read-more", "truncate-words-number"])
252
+ }, null, 8, ["use", "loan-amount", "status", "borrower-count", "name", "distribution-model", "country", "show-read-more", "truncate-words-number", "max-lines"])
252
253
  ], 2))
253
254
  ]),
254
255
  _: 1
@@ -318,7 +319,7 @@ function lt(b, n, e, t, rt, x) {
318
319
  t.handleSelectedAmount
319
320
  ],
320
321
  "aria-label": "Lend amount",
321
- disabled: !t.amountOptions.length || x.isInBasket,
322
+ disabled: !t.amountOptions.length || _.isInBasket,
322
323
  class: "amount-dropdown"
323
324
  }, {
324
325
  default: f(() => [
@@ -410,7 +411,7 @@ function lt(b, n, e, t, rt, x) {
410
411
  ], 2)
411
412
  ], 10, S);
412
413
  }
413
- const mt = /* @__PURE__ */ B(D, [["render", lt], ["__scopeId", "data-v-330ff302"]]);
414
+ const mt = /* @__PURE__ */ B(D, [["render", lt], ["__scopeId", "data-v-6313c6b8"]]);
414
415
  export {
415
416
  gt as KV_COMPACT_LOAN_CARD_FRAGMENT,
416
417
  ft as KV_COMPACT_LOAN_CARD_USER_FRAGMENT,
@@ -200,6 +200,18 @@ const d = {
200
200
  type: Number,
201
201
  default: 0
202
202
  },
203
+ /**
204
+ * Opt-in line cap for the loan use statement. Unset, the light view behaves as before: a 4-line
205
+ * CSS clamp plus the `truncateWordsNumber` word cut. Set, a statement that runs past the cap is
206
+ * trimmed so "… read more" stays visible; one that fills the cap on its own is shown whole
207
+ * without the link. The `light-detailed` variant always clamps to 3 lines and ignores this prop.
208
+ */
209
+ loanUseMaxLines: {
210
+ type: Number,
211
+ default: null,
212
+ validator: (e) => e === null || e > 0
213
+ // 0 would hide the whole statement
214
+ },
203
215
  showRefreshButton: {
204
216
  type: Boolean,
205
217
  default: !1
@@ -236,8 +248,8 @@ const d = {
236
248
  distributionModel: v,
237
249
  formattedLocation: g,
238
250
  fundraisingPercent: B,
239
- hasProgressData: _,
240
- imageHash: N,
251
+ hasProgressData: N,
252
+ imageHash: _,
241
253
  isLoading: T,
242
254
  loanAmount: f,
243
255
  loanBorrowerCount: L,
@@ -245,9 +257,9 @@ const d = {
245
257
  loanStatus: y,
246
258
  loanUse: A,
247
259
  readMorePath: R,
248
- tag: F,
260
+ tag: M,
249
261
  unreservedAmount: u,
250
- sharesAvailable: M
262
+ sharesAvailable: F
251
263
  } = Z(e), { clickReadMore: K } = ee(e, o), P = a(() => c.value ? void 0 : { width: "100px", height: "100px" }), D = a(() => {
252
264
  if (!e.website)
253
265
  return "";
@@ -269,7 +281,7 @@ const d = {
269
281
  loanStatus: y.value,
270
282
  loanUse: A.value
271
283
  });
272
- }), O = a(() => i.value.borrowerNameWithCountry), U = a(() => i.value.loanUsePrefixStart), V = a(() => i.value.loanUsePrefixEnd), x = a(() => i.value.statementTitle), I = a(() => i.value.visibleUseStatement), C = a(() => {
284
+ }), U = a(() => i.value.borrowerNameWithCountry), O = a(() => i.value.loanUsePrefixStart), V = a(() => i.value.loanUsePrefixEnd), x = a(() => i.value.statementTitle), I = a(() => i.value.visibleUseStatement), C = a(() => {
273
285
  var r;
274
286
  const t = parseFloat(u.value);
275
287
  if (!t) return [];
@@ -310,14 +322,14 @@ const d = {
310
322
  amountOptions: C,
311
323
  borrowerImageStyle: P,
312
324
  borrowerName: h,
313
- borrowerNameWithCountry: O,
325
+ borrowerNameWithCountry: U,
314
326
  lightDetailedAmount: G,
315
327
  distributionModel: v,
316
328
  formattedLocation: g,
317
329
  formattedWebsite: D,
318
330
  fundraisingPercent: B,
319
- hasProgressData: _,
320
- imageHash: N,
331
+ hasProgressData: N,
332
+ imageHash: _,
321
333
  isLightDetailedVariant: s,
322
334
  isLoading: T,
323
335
  isPostGoalVariant: m,
@@ -327,15 +339,15 @@ const d = {
327
339
  loanStatus: y,
328
340
  loanUse: A,
329
341
  loanUsePrefixEnd: V,
330
- loanUsePrefixStart: U,
342
+ loanUsePrefixStart: O,
331
343
  mdiLink: J,
332
344
  mdiCached: z,
333
345
  mdiArrowRight: H,
334
346
  readMorePath: R,
335
347
  shouldShowRefreshButton: w,
336
- tag: F,
348
+ tag: M,
337
349
  unreservedAmount: u,
338
- sharesAvailable: M,
350
+ sharesAvailable: F,
339
351
  selectedAmount: l,
340
352
  statementTitle: x,
341
353
  customStyle: E,
@@ -132,6 +132,11 @@ declare const _default: import('vue').DefineComponent<import('vue').ExtractPropT
132
132
  type: NumberConstructor;
133
133
  default: number;
134
134
  };
135
+ loanUseMaxLines: {
136
+ type: NumberConstructor;
137
+ default: any;
138
+ validator: (value: number | null) => boolean;
139
+ };
135
140
  showRefreshButton: {
136
141
  type: BooleanConstructor;
137
142
  default: boolean;
@@ -326,6 +331,11 @@ declare const _default: import('vue').DefineComponent<import('vue').ExtractPropT
326
331
  type: NumberConstructor;
327
332
  default: number;
328
333
  };
334
+ loanUseMaxLines: {
335
+ type: NumberConstructor;
336
+ default: any;
337
+ validator: (value: number | null) => boolean;
338
+ };
329
339
  showRefreshButton: {
330
340
  type: BooleanConstructor;
331
341
  default: boolean;
@@ -377,6 +387,7 @@ declare const _default: import('vue').DefineComponent<import('vue').ExtractPropT
377
387
  website: string;
378
388
  showLoanUse: boolean;
379
389
  showLightView: boolean;
390
+ loanUseMaxLines: number;
380
391
  showRefreshButton: boolean;
381
392
  customAmountLent: string;
382
393
  }, {}, {
@@ -503,13 +514,22 @@ declare const _default: import('vue').DefineComponent<import('vue').ExtractPropT
503
514
  type: NumberConstructor;
504
515
  default: number;
505
516
  };
517
+ maxLines: {
518
+ type: NumberConstructor;
519
+ default: any;
520
+ validator: (value: number | null) => boolean;
521
+ };
506
522
  }>, {}, {}, {
523
+ hasMaxLines(): boolean;
524
+ fitsReadMore(): boolean;
507
525
  helpLanguage(): "helps" | "helped";
508
526
  isDirect(): boolean;
509
527
  whySpecialSentence(): string;
510
528
  nameSpan(): string;
511
529
  loanUse(): string;
512
- }, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
530
+ }, {
531
+ fitReadMore(): void;
532
+ }, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
513
533
  anonymizationLevel: {
514
534
  type: StringConstructor;
515
535
  default: string;
@@ -570,6 +590,11 @@ declare const _default: import('vue').DefineComponent<import('vue').ExtractPropT
570
590
  type: NumberConstructor;
571
591
  default: number;
572
592
  };
593
+ maxLines: {
594
+ type: NumberConstructor;
595
+ default: any;
596
+ validator: (value: number | null) => boolean;
597
+ };
573
598
  }>> & Readonly<{}>, {
574
599
  name: string;
575
600
  use: string;
@@ -586,6 +611,7 @@ declare const _default: import('vue').DefineComponent<import('vue').ExtractPropT
586
611
  useIndicativeHelpText: boolean;
587
612
  showReadMore: boolean;
588
613
  truncateWordsNumber: number;
614
+ maxLines: number;
589
615
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
590
616
  KvMaterialIcon: import('vue').DefineComponent<import('vue').ExtractPropTypes<{
591
617
  icon: {
@@ -1 +1 @@
1
- .image-upload-icon[data-v-ea802cf6]{background-color:#ffffffbf;border-radius:9px;display:flex;align-items:center;justify-content:center;border-radius:.5rem}.image-upload-icon--circle[data-v-ea802cf6]{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));--tw-shadow: 0 1px 4px 0 rgb(0 0 0 / .08);--tw-shadow-colored: 0 1px 4px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}
1
+ .image-upload-icon[data-v-373dd692]{background-color:#ffffffbf;border-radius:9px;display:flex;align-items:center;justify-content:center;border-radius:.5rem}.image-upload-icon--circle[data-v-373dd692]{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));--tw-shadow: 0 1px 4px 0 rgb(0 0 0 / .08);--tw-shadow-colored: 0 1px 4px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}
@@ -1,72 +1,85 @@
1
- import p from "./KvImageUpload.script.js";
2
- import { resolveComponent as f, openBlock as i, createElementBlock as n, createElementVNode as a, normalizeStyle as g, normalizeClass as l, renderSlot as b, createVNode as c, withModifiers as w, createCommentVNode as m } from "vue";
1
+ import v from "./KvImageUpload.script.js";
2
+ import { resolveComponent as f, openBlock as t, createElementBlock as r, createElementVNode as i, normalizeStyle as b, normalizeClass as l, renderSlot as c, createVNode as s, toDisplayString as p, withModifiers as m, createCommentVNode as w } from "vue";
3
3
  import "./KvImageUpload.css";
4
- import u from "../_virtual/_plugin-vue_export-helper.js";
5
- const v = { class: "tw-flex tw-flex-col" }, h = ["src", "alt"], C = ["accept", "aria-label"];
6
- function k(d, t, s, e, I, _) {
7
- const r = f("kv-material-icon");
8
- return i(), n("div", v, [
9
- a("div", {
10
- class: l(["kv-image-upload tw-relative", e.shapeClass]),
11
- style: g(e.containerStyle)
4
+ import D from "../_virtual/_plugin-vue_export-helper.js";
5
+ const C = { class: "tw-flex tw-flex-col" }, k = ["src", "alt"], y = { class: "tw-text-label" }, I = ["accept", "aria-label"];
6
+ function h(n, o, d, e, u, O) {
7
+ const g = f("kv-material-icon");
8
+ return t(), r("div", C, [
9
+ i("div", {
10
+ class: l(["kv-image-upload tw-relative", [e.shapeClass, { "kv-image-upload--dragging": e.isDraggingOver }]]),
11
+ style: b(e.containerStyle),
12
+ onDragenter: o[3] || (o[3] = (...a) => e.onDragEnter && e.onDragEnter(...a)),
13
+ onDragover: o[4] || (o[4] = (...a) => e.onDragOver && e.onDragOver(...a)),
14
+ onDragleave: o[5] || (o[5] = (...a) => e.onDragLeave && e.onDragLeave(...a)),
15
+ onDrop: o[6] || (o[6] = (...a) => e.onDrop && e.onDrop(...a))
12
16
  }, [
13
- e.previewImage ? (i(), n("img", {
17
+ e.previewImage ? (t(), r("img", {
14
18
  key: 0,
15
19
  src: e.previewImage,
16
- alt: s.imageAlt,
20
+ alt: d.imageAlt,
17
21
  class: l(["tw-w-full tw-h-full tw-object-cover", e.shapeClass])
18
- }, null, 10, h)) : b(d.$slots, "fallback-image", { key: 1 }, () => [
19
- a("div", {
22
+ }, null, 10, k)) : c(n.$slots, "fallback-image", {
23
+ key: 1,
24
+ isDraggingOver: e.isDraggingOver
25
+ }, () => [
26
+ i("div", {
20
27
  class: l(["kv-image-upload__placeholder tw-w-full tw-h-full tw-bg-eco-green-1 tw-p-0.5", e.shapeClass])
21
28
  }, [
22
- a("div", {
23
- class: l(["tw-flex tw-flex-col tw-items-center tw-justify-center tw-gap-0.5 tw-w-full tw-h-full tw-border-2 tw-border-dashed tw-border-black", e.shapeClass])
29
+ i("div", {
30
+ class: l(["tw-flex tw-flex-col tw-items-center tw-justify-center tw-gap-0.5 tw-w-full tw-h-full tw-border-2 tw-border-dashed", [e.shapeClass, e.isDraggingOver ? "tw-border-action" : "tw-border-black"]])
24
31
  }, [
25
- c(r, {
32
+ s(g, {
26
33
  icon: e.mdiCameraPlusOutline,
27
34
  class: "tw-w-3.5"
28
35
  }, null, 8, ["icon"]),
29
- t[3] || (t[3] = a("span", { class: "tw-text-label" }, " Add a photo ", -1))
36
+ i("span", y, p(e.isDraggingOver ? "Drop to upload" : "Add a photo"), 1)
30
37
  ], 2)
31
38
  ], 2)
32
39
  ], !0),
33
- a("input", {
40
+ i("input", {
34
41
  ref: "fileInput",
35
42
  type: "file",
36
43
  accept: e.acceptAttr,
37
44
  "aria-label": e.inputLabel,
38
45
  class: "tw-absolute tw-inset-0 tw-w-full tw-h-full tw-opacity-0 tw-cursor-pointer",
39
- onChange: t[0] || (t[0] = (...o) => e.handleFileChange && e.handleFileChange(...o))
40
- }, null, 40, C),
41
- e.previewImage || s.showEditIcon ? (i(), n("button", {
46
+ onChange: o[0] || (o[0] = (...a) => e.handleFileChange && e.handleFileChange(...a))
47
+ }, null, 40, I),
48
+ e.previewImage || d.showEditIcon ? (t(), r("button", {
42
49
  key: 2,
43
- class: l(["image-upload-icon edit-icon tw-absolute tw-bottom-1 tw-right-1 tw-p-1 tw-z-10", { "image-upload-icon--circle": e.isCircle }]),
50
+ class: l(["image-upload-icon edit-icon tw-absolute tw-bottom-1 tw-right-1 tw-p-1 tw-z-1", { "image-upload-icon--circle": e.isCircle }]),
44
51
  type: "button",
45
52
  "aria-hidden": "true",
46
53
  tabindex: "-1",
47
- onClick: t[1] || (t[1] = w((...o) => e.openFileInput && e.openFileInput(...o), ["stop"]))
54
+ onClick: o[1] || (o[1] = m((...a) => e.openFileInput && e.openFileInput(...a), ["stop"]))
48
55
  }, [
49
- c(r, {
56
+ s(g, {
50
57
  icon: e.mdiPencil,
51
58
  class: "tw-w-2"
52
59
  }, null, 8, ["icon"])
53
- ], 2)) : m("", !0),
54
- e.previewImage ? (i(), n("button", {
60
+ ], 2)) : w("", !0),
61
+ e.previewImage ? (t(), r("button", {
55
62
  key: 3,
56
- class: l(["image-upload-icon remove-icon tw-absolute tw-top-1 tw-right-1 tw-p-1 tw-z-10", { "image-upload-icon--circle": e.isCircle }]),
63
+ class: l(["image-upload-icon remove-icon tw-absolute tw-top-1 tw-right-1 tw-p-1 tw-z-1", { "image-upload-icon--circle": e.isCircle }]),
57
64
  type: "button",
58
65
  "aria-label": "Remove Image",
59
- onClick: t[2] || (t[2] = w((...o) => e.removeImage && e.removeImage(...o), ["stop"]))
66
+ onClick: o[2] || (o[2] = m((...a) => e.removeImage && e.removeImage(...a), ["stop"]))
60
67
  }, [
61
- c(r, {
68
+ s(g, {
62
69
  icon: e.mdiClose,
63
70
  class: "tw-w-2"
64
71
  }, null, 8, ["icon"])
65
- ], 2)) : m("", !0)
66
- ], 6)
72
+ ], 2)) : w("", !0),
73
+ e.isDraggingOver && e.previewImage && n.$slots["drag-overlay"] ? (t(), r("div", {
74
+ key: 4,
75
+ class: l(["tw-absolute tw-inset-0 tw-z-2 tw-pointer-events-none", e.shapeClass])
76
+ }, [
77
+ c(n.$slots, "drag-overlay", {}, void 0, !0)
78
+ ], 2)) : w("", !0)
79
+ ], 38)
67
80
  ]);
68
81
  }
69
- const A = /* @__PURE__ */ u(p, [["render", k], ["__scopeId", "data-v-ea802cf6"]]);
82
+ const F = /* @__PURE__ */ D(v, [["render", h], ["__scopeId", "data-v-373dd692"]]);
70
83
  export {
71
- A as default
84
+ F as default
72
85
  };
@@ -1,10 +1,10 @@
1
- import { toRefs as S, ref as u, watch as x, computed as l } from "vue";
2
- import { mdiCameraPlusOutline as F, mdiClose as w, mdiPencil as I } from "@mdi/js";
3
- import C from "./KvMaterialIcon.js";
4
- import { DEFAULT_ACCEPTED_IMAGE_TYPES as b, validateImageFile as R, cropResizeImageToDataUrl as T } from "../utils/imageUtils.js";
5
- const U = {
1
+ import { toRefs as T, ref as f, watch as w, computed as o } from "vue";
2
+ import { mdiCameraPlusOutline as E, mdiClose as I, mdiPencil as C } from "@mdi/js";
3
+ import b from "./KvMaterialIcon.js";
4
+ import { DEFAULT_ACCEPTED_IMAGE_TYPES as R, validateImageFile as z, cropResizeImageToDataUrl as A } from "../utils/imageUtils.js";
5
+ const H = {
6
6
  components: {
7
- KvMaterialIcon: C
7
+ KvMaterialIcon: b
8
8
  },
9
9
  props: {
10
10
  /**
@@ -27,8 +27,8 @@ const U = {
27
27
  shape: {
28
28
  type: String,
29
29
  default: "square",
30
- validator(e) {
31
- return ["square", "circle"].includes(e);
30
+ validator(a) {
31
+ return ["square", "circle"].includes(a);
32
32
  }
33
33
  },
34
34
  /**
@@ -59,7 +59,7 @@ const U = {
59
59
  */
60
60
  acceptedFileTypes: {
61
61
  type: Array,
62
- default: () => [...b]
62
+ default: () => [...R]
63
63
  },
64
64
  /**
65
65
  * Whether to show the edit (pencil) affordance in the blank state, before an image is
@@ -92,59 +92,85 @@ const U = {
92
92
  */
93
93
  "file-error"
94
94
  ],
95
- setup(e, { emit: r }) {
96
- const { imageUrl: c } = S(e), a = u(c.value || ""), s = u(null);
97
- x(c, (t) => {
98
- a.value = t || "";
95
+ setup(a, { emit: s }) {
96
+ const { imageUrl: d } = T(a), n = f(d.value || ""), m = f(null), c = f(!1);
97
+ let l = 0;
98
+ w(d, (e) => {
99
+ n.value = e || "";
99
100
  });
100
- const m = l(() => e.shape === "circle"), d = l(() => m.value ? "tw-rounded-full" : "tw-rounded"), p = l(() => e.acceptedFileTypes.join(",")), f = l(() => a.value ? "Change image" : "Upload image"), g = l(() => ({
101
- aspectRatio: String(e.aspectRatio),
102
- maxHeight: `${e.previewSize}px`,
103
- maxWidth: `${e.previewSize * e.aspectRatio}px`
104
- })), v = () => {
105
- var t;
106
- (t = s.value) == null || t.click();
107
- }, y = () => {
108
- a.value = "", r("file-removed");
109
- }, h = async (t) => {
110
- const { valid: o, error: i } = R(t, {
111
- maxSizeMb: e.maxSizeMb,
112
- acceptedFileTypes: e.acceptedFileTypes
101
+ const p = o(() => a.shape === "circle"), v = o(() => p.value ? "tw-rounded-full" : "tw-rounded"), y = o(() => a.acceptedFileTypes.join(",")), D = o(() => n.value ? "Change image" : "Upload image"), h = o(() => ({
102
+ aspectRatio: String(a.aspectRatio),
103
+ maxHeight: `${a.previewSize}px`,
104
+ maxWidth: `${a.previewSize * a.aspectRatio}px`
105
+ })), x = () => {
106
+ var e;
107
+ (e = m.value) == null || e.click();
108
+ }, F = () => {
109
+ n.value = "", s("file-removed");
110
+ }, g = async (e) => {
111
+ const { valid: t, error: r } = z(e, {
112
+ maxSizeMb: a.maxSizeMb,
113
+ acceptedFileTypes: a.acceptedFileTypes
113
114
  });
114
- if (!o && i) {
115
- r("file-error", i);
115
+ if (!t && r) {
116
+ s("file-error", r);
116
117
  return;
117
118
  }
118
119
  try {
119
- a.value = await T(t, {
120
- aspectRatio: e.aspectRatio,
121
- maxDimension: e.maxDimension
122
- }), r("file-uploaded", { file: t });
120
+ n.value = await A(e, {
121
+ aspectRatio: a.aspectRatio,
122
+ maxDimension: a.maxDimension
123
+ }), s("file-uploaded", { file: e });
123
124
  } catch {
124
- r("file-error", { type: "other", message: "Failed to read file" });
125
+ s("file-error", { type: "other", message: "Failed to read file" });
125
126
  }
127
+ }, S = (e) => {
128
+ var i;
129
+ const t = e.target, r = (i = t.files) == null ? void 0 : i[0];
130
+ r && (g(r), t.value = "");
131
+ }, u = (e) => {
132
+ var t;
133
+ return Array.from(((t = e.dataTransfer) == null ? void 0 : t.types) ?? []).includes("Files");
126
134
  };
127
135
  return {
128
- mdiPencil: I,
129
- mdiClose: w,
130
- mdiCameraPlusOutline: F,
131
- fileInput: s,
132
- previewImage: a,
133
- isCircle: m,
134
- shapeClass: d,
135
- acceptAttr: p,
136
- inputLabel: f,
137
- containerStyle: g,
138
- openFileInput: v,
139
- removeImage: y,
140
- handleFileChange: (t) => {
141
- var n;
142
- const o = t.target, i = (n = o.files) == null ? void 0 : n[0];
143
- i && (h(i), o.value = "");
136
+ mdiPencil: C,
137
+ mdiClose: I,
138
+ mdiCameraPlusOutline: E,
139
+ fileInput: m,
140
+ previewImage: n,
141
+ isCircle: p,
142
+ isDraggingOver: c,
143
+ shapeClass: v,
144
+ acceptAttr: y,
145
+ inputLabel: D,
146
+ containerStyle: h,
147
+ openFileInput: x,
148
+ removeImage: F,
149
+ handleFileChange: S,
150
+ onDragEnter: (e) => {
151
+ u(e) && (l += 1, c.value = !0);
152
+ },
153
+ onDragOver: (e) => {
154
+ if (!u(e))
155
+ return;
156
+ e.preventDefault();
157
+ const { dataTransfer: t } = e;
158
+ t && (t.dropEffect = "copy");
159
+ },
160
+ onDragLeave: (e) => {
161
+ u(e) && (l = Math.max(0, l - 1), l === 0 && (c.value = !1));
162
+ },
163
+ onDrop: (e) => {
164
+ var r, i;
165
+ if (!u(e))
166
+ return;
167
+ e.preventDefault(), l = 0, c.value = !1;
168
+ const t = (i = (r = e.dataTransfer) == null ? void 0 : r.files) == null ? void 0 : i[0];
169
+ t && g(t);
144
170
  }
145
171
  };
146
172
  }
147
173
  };
148
174
  export {
149
- U as default
175
+ H as default
150
176
  };
@@ -0,0 +1 @@
1
+ .kv-loan-use-lines[data-v-534d5054]{-webkit-line-clamp:var(--kv-loan-use-lines);line-clamp:var(--kv-loan-use-lines)}
@@ -1,16 +1,18 @@
1
- import o from "./KvLoanUse.script.js";
2
- import { KV_LOAN_USE_FRAGMENT as E } from "./KvLoanUse.script.js";
3
- import { openBlock as r, createElementBlock as n } from "vue";
4
- import t from "../_virtual/_plugin-vue_export-helper.js";
5
- const c = ["innerHTML"];
6
- function s(_, p, a, l, m, e) {
7
- return r(), n("p", {
8
- class: "tw-line-clamp-4",
1
+ import a from "./KvLoanUse.script.js";
2
+ import { KV_LOAN_USE_FRAGMENT as k } from "./KvLoanUse.script.js";
3
+ import { openBlock as s, createElementBlock as o, normalizeStyle as r, normalizeClass as t } from "vue";
4
+ import "./KvLoanUse.css";
5
+ import l from "../_virtual/_plugin-vue_export-helper.js";
6
+ const i = ["innerHTML"];
7
+ function c(_, m, n, p, d, e) {
8
+ return s(), o("p", {
9
+ class: t(["tw-line-clamp-4", { "kv-loan-use-lines": e.hasMaxLines }]),
10
+ style: r(e.hasMaxLines ? { "--kv-loan-use-lines": n.maxLines } : void 0),
9
11
  innerHTML: e.loanUse
10
- }, null, 8, c);
12
+ }, null, 14, i);
11
13
  }
12
- const u = /* @__PURE__ */ t(o, [["render", s]]);
14
+ const v = /* @__PURE__ */ l(a, [["render", c], ["__scopeId", "data-v-534d5054"]]);
13
15
  export {
14
- E as KV_LOAN_USE_FRAGMENT,
15
- u as default
16
+ k as KV_LOAN_USE_FRAGMENT,
17
+ v as default
16
18
  };
@@ -1,7 +1,8 @@
1
- import u from "graphql-tag";
1
+ import h from "graphql-tag";
2
2
  import l from "numeral";
3
- import { truncateStringByWords as i } from "../utils/loanUtils.js";
4
- const r = "direct", o = (e) => e.charAt(0).toUpperCase() + e.slice(1), t = (e) => e.charAt(0).toLowerCase() + e.slice(1), m = u`
3
+ import { truncateStringByWords as a } from "../utils/loanUtils.js";
4
+ import { fitStatement as d, READ_MORE_CLASS as r } from "../utils/loanUseFit.js";
5
+ const o = "direct", u = (e) => e.charAt(0).toUpperCase() + e.slice(1), t = (e) => e.charAt(0).toLowerCase() + e.slice(1), w = h`
5
6
  fragment KvLoanUse on LoanBasic {
6
7
  id
7
8
  anonymizationLevel
@@ -13,7 +14,7 @@ const r = "direct", o = (e) => e.charAt(0).toUpperCase() + e.slice(1), t = (e) =
13
14
  distributionModel
14
15
  whySpecial
15
16
  }
16
- `, f = {
17
+ `, S = {
17
18
  name: "KvLoanUse",
18
19
  props: {
19
20
  anonymizationLevel: {
@@ -42,7 +43,7 @@ const r = "direct", o = (e) => e.charAt(0).toUpperCase() + e.slice(1), t = (e) =
42
43
  },
43
44
  distributionModel: {
44
45
  type: String,
45
- default: r
46
+ default: o
46
47
  },
47
48
  whySpecial: {
48
49
  type: String,
@@ -76,14 +77,33 @@ const r = "direct", o = (e) => e.charAt(0).toUpperCase() + e.slice(1), t = (e) =
76
77
  truncateWordsNumber: {
77
78
  type: Number,
78
79
  default: 0
80
+ },
81
+ /**
82
+ * Opt-in line cap. Unset, the statement keeps the plain 4-line CSS clamp and none of the fit
83
+ * logic runs. Set, the clamp follows this number and, with `showReadMore`, a statement that runs
84
+ * past the cap is trimmed word by word after render so "… read more" ends the last visible line;
85
+ * one that fills the cap on its own is shown whole without the link.
86
+ */
87
+ maxLines: {
88
+ type: Number,
89
+ default: null,
90
+ validator: (e) => e === null || e > 0
91
+ // 0 would hide the whole statement
79
92
  }
80
93
  },
81
94
  computed: {
95
+ hasMaxLines() {
96
+ return this.maxLines !== null;
97
+ },
98
+ // The fit only applies to an opted-in cap with a link to fit; hideBorrowerDetails never renders one.
99
+ fitsReadMore() {
100
+ return this.hasMaxLines && this.showReadMore && !this.hideBorrowerDetails;
101
+ },
82
102
  helpLanguage() {
83
103
  return this.status === "fundraising" || this.status === "inactive" || this.status === "reviewed" ? "helps" : "helped";
84
104
  },
85
105
  isDirect() {
86
- return this.distributionModel === r;
106
+ return this.distributionModel === o;
87
107
  },
88
108
  whySpecialSentence() {
89
109
  return this.whySpecial ? ` This loan is special because ${t(this.whySpecial)}` : "";
@@ -95,19 +115,49 @@ const r = "direct", o = (e) => e.charAt(0).toUpperCase() + e.slice(1), t = (e) =
95
115
  if (this.anonymizationLevel === "full" || this.use.length === 0)
96
116
  return "For the borrower's privacy, this loan has been made anonymous.";
97
117
  if (this.hideBorrowerDetails)
98
- return `${o(this.helpLanguage)} ${t(this.use)}${this.whySpecialSentence}`;
118
+ return `${u(this.helpLanguage)} ${t(this.use)}${this.whySpecialSentence}`;
99
119
  if (this.hideLoanAmount) {
100
120
  const n = this.useIndicativeHelpText ? this.helpLanguage : "Help";
101
- let a = `${o(n)} ${this.nameSpan} ${t(this.use)} ${this.whySpecialSentence}`;
102
- return this.showReadMore && (a = `${i(a, this.truncateWordsNumber)} <span class=" tw-text-action tw-underline">read more</span>`), a;
121
+ let i = `${u(n)} ${this.nameSpan} ${t(this.use)} ${this.whySpecialSentence}`;
122
+ return this.showReadMore && (i = `${a(i, this.truncateWordsNumber)} <span class="${r} tw-text-action tw-underline">read more</span>`), i;
103
123
  }
104
124
  const e = this.borrowerCount > 1;
105
125
  let s = `${l(this.loanAmount).format("$0,0")} ${this.isDirect ? "to" : this.helpLanguage} ${e ? "a member of " : ""}${this.nameSpan} ${this.isDirect ? `${this.helpLanguage} ` : ""}${t(this.use)}${this.whySpecialSentence}`;
106
- return this.showReadMore && (s = `${i(s, this.truncateWordsNumber)} <span class=" tw-text-action tw-underline">read more</span>`), s;
126
+ return this.showReadMore && (s = `${a(s, this.truncateWordsNumber)} <span class="${r} tw-text-action tw-underline">read more</span>`), s;
127
+ }
128
+ },
129
+ watch: {
130
+ loanUse: {
131
+ // Runs after Vue has written the full statement to the DOM but within the same flush, so the
132
+ // fitted text is in place before the browser paints.
133
+ handler() {
134
+ this.fitReadMore();
135
+ },
136
+ flush: "post"
137
+ }
138
+ },
139
+ mounted() {
140
+ if (this.fitsReadMore) {
141
+ if (typeof ResizeObserver > "u") {
142
+ this.fitReadMore();
143
+ return;
144
+ }
145
+ this.resizeObserver = new ResizeObserver(() => this.fitReadMore()), this.resizeObserver.observe(this.$el);
146
+ }
147
+ },
148
+ beforeUnmount() {
149
+ var e;
150
+ (e = this.resizeObserver) == null || e.disconnect();
151
+ },
152
+ methods: {
153
+ fitReadMore() {
154
+ if (!this.fitsReadMore) return;
155
+ const e = this.$el;
156
+ !e || e.clientHeight === 0 || d(e, this.loanUse);
107
157
  }
108
158
  }
109
159
  };
110
160
  export {
111
- m as KV_LOAN_USE_FRAGMENT,
112
- f as default
161
+ w as KV_LOAN_USE_FRAGMENT,
162
+ S as default
113
163
  };
@@ -60,13 +60,22 @@ declare const _default: import('vue').DefineComponent<import('vue').ExtractPropT
60
60
  type: NumberConstructor;
61
61
  default: number;
62
62
  };
63
+ maxLines: {
64
+ type: NumberConstructor;
65
+ default: any;
66
+ validator: (value: number | null) => boolean;
67
+ };
63
68
  }>, {}, {}, {
69
+ hasMaxLines(): boolean;
70
+ fitsReadMore(): boolean;
64
71
  helpLanguage(): "helps" | "helped";
65
72
  isDirect(): boolean;
66
73
  whySpecialSentence(): string;
67
74
  nameSpan(): string;
68
75
  loanUse(): string;
69
- }, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
76
+ }, {
77
+ fitReadMore(): void;
78
+ }, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<{
70
79
  anonymizationLevel: {
71
80
  type: StringConstructor;
72
81
  default: string;
@@ -127,6 +136,11 @@ declare const _default: import('vue').DefineComponent<import('vue').ExtractPropT
127
136
  type: NumberConstructor;
128
137
  default: number;
129
138
  };
139
+ maxLines: {
140
+ type: NumberConstructor;
141
+ default: any;
142
+ validator: (value: number | null) => boolean;
143
+ };
130
144
  }>> & Readonly<{}>, {
131
145
  name: string;
132
146
  use: string;
@@ -143,5 +157,6 @@ declare const _default: import('vue').DefineComponent<import('vue').ExtractPropT
143
157
  useIndicativeHelpText: boolean;
144
158
  showReadMore: boolean;
145
159
  truncateWordsNumber: number;
160
+ maxLines: number;
146
161
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
147
162
  export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kiva/kv-components",
3
- "version": "10.6.1",
3
+ "version": "10.8.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -116,5 +116,5 @@
116
116
  "dependencies": {
117
117
  "fuse.js": "^7.1.0"
118
118
  },
119
- "gitHead": "a55163da474c53d2958af25e6b68684b3f1922db"
119
+ "gitHead": "3bf4173c32eeef15fa827502a287df142343b445"
120
120
  }