@crediball/elements 0.10.0 → 0.11.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.
@@ -217,13 +217,24 @@ var STYLE = `
217
217
  .card {
218
218
  width: 100%;
219
219
  max-width: 420px;
220
+ /* Cap to the overlay's own (viewport-correct, since it's fixed/inset:0)
221
+ content box and scroll internally past that \u2014 otherwise a host with
222
+ many optional sections enabled (balance, subscribe, promo code,
223
+ packages, custom amount, auto top-up, referral) can produce a card
224
+ taller than the mobile viewport, which the centered overlay then
225
+ clips top and bottom with no way to reach the rest. */
226
+ max-height: 100%;
227
+ overflow-y: auto;
228
+ -webkit-overflow-scrolling: touch;
229
+ overscroll-behavior: contain;
230
+ box-sizing: border-box;
220
231
  padding: 32px;
221
232
  border-radius: var(--crediball-radius-card, 18px);
222
233
  background: var(--crediball-canvas, #ffffff);
223
234
  color: var(--crediball-ink, #1d1d1f);
224
235
  }
225
236
  .balance-row { margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid var(--crediball-hairline, #e0e0e0); }
226
- .eyebrow { font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--crediball-ink-muted, #7a7a7a); margin-bottom: 4px; }
237
+ .eyebrow { font-size: 11px; font-weight: 600; letter-spacing: 0.06em; color: var(--crediball-ink-muted, #7a7a7a); margin-bottom: 4px; }
227
238
  .balance-value { font-size: 22px; font-weight: 600; }
228
239
  h3 { margin: 0; font-size: 24px; font-weight: 600; letter-spacing: -0.01em; }
229
240
  p.desc { margin: 8px 0 0; font-size: 14px; color: var(--crediball-ink-muted, #7a7a7a); }
@@ -236,14 +247,17 @@ var STYLE = `
236
247
  display: flex;
237
248
  justify-content: space-between;
238
249
  align-items: center;
250
+ gap: 16px;
239
251
  background: var(--crediball-accent, #0066cc);
240
252
  color: var(--crediball-on-accent, #ffffff);
241
253
  font: inherit;
242
- font-size: 17px;
243
254
  padding: 11px 22px;
244
255
  border-radius: var(--crediball-radius, 9999px);
245
256
  }
246
- button.option .meta { opacity: 0.85; font-size: 14px; }
257
+ .option-left { display: flex; flex-direction: column; align-items: flex-start; flex: 1; min-width: 0; }
258
+ .option-name { font-size: 17px; font-weight: 600; line-height: 1.2; }
259
+ .option-credits { font-size: 13px; opacity: 0.85; line-height: 1.2; }
260
+ .option-price { font-size: 17px; font-weight: 600; white-space: nowrap; flex-shrink: 0; }
247
261
  .custom-row { display: flex; gap: 8px; align-items: center; margin-top: 12px; }
248
262
  .custom-row .sym { font-size: 14px; color: var(--crediball-ink-muted, #7a7a7a); }
249
263
  .custom-row input {
@@ -304,6 +318,8 @@ var STYLE = `
304
318
  color: var(--crediball-ink, #1d1d1f);
305
319
  outline: none;
306
320
  }
321
+ .promo-code-input.invalid { border-color: var(--crediball-accent, #0066cc); }
322
+ .promo-code-error { margin: 6px 0 0; font-size: 12px; color: var(--crediball-accent, #0066cc); }
307
323
  .autotopup { margin-top: 16px; }
308
324
  .autotopup-toggle {
309
325
  appearance: none; border: none; cursor: pointer; background: transparent; padding: 0;
@@ -326,7 +342,7 @@ var STYLE = `
326
342
  }
327
343
  .autotopup-hint { margin: 6px 0 0; font-size: 11px; color: var(--crediball-ink-muted, #7a7a7a); }
328
344
  .referral { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--crediball-hairline, #e0e0e0); }
329
- .referral-label { margin-bottom: 8px; font-size: 12px; font-weight: 600; color: var(--crediball-ink-muted, #7a7a7a); }
345
+ .referral-label { margin-bottom: 8px; font-size: 12px; font-weight: 600; color: var(--crediball-ink-muted, #7a7a7a); white-space: pre-line; }
330
346
  .referral-row { display: flex; gap: 8px; align-items: center; }
331
347
  .referral-row .link {
332
348
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
@@ -414,6 +430,10 @@ var CrediballPaywallElement = class extends CrediballElement {
414
430
  const plans = pkgs?.activeSubscription ? [] : pkgs?.subscriptionPlans ?? [];
415
431
  const custom = pkgs?.customTopup;
416
432
  const showCustom = Boolean(custom?.enabled);
433
+ const referralReward = this.referral?.rewardCredits ?? 0;
434
+ const referredReward = this.referral?.referredRewardCredits ?? 0;
435
+ const defaultReferralLabel = referredReward > 0 ? "Refer a friend.\nYou earn {credits} credits and your friend {referredCredits} credits" : "Refer a friend \xB7 earn {credits} credits";
436
+ const referralLabel = (this.getAttribute("referral-label") ?? defaultReferralLabel).replace("{credits}", formatCredits(referralReward)).replace("{referredCredits}", formatCredits(referredReward));
417
437
  this.shadow.innerHTML = `
418
438
  <style>${STYLE}</style>
419
439
  <div class="overlay" part="overlay">
@@ -428,18 +448,23 @@ var CrediballPaywallElement = class extends CrediballElement {
428
448
  ${plans.map(
429
449
  (p) => `
430
450
  <button class="option" data-kind="plan" data-id="${escapeHtml(p.id)}">
431
- <span>${escapeHtml(p.name)}</span>
432
- <span class="meta">${formatMoney(p.price, currency)}/${PERIOD_SUFFIX[p.period] ?? p.period} \xB7 ${formatCredits(p.credits)} cr</span>
451
+ <div class="option-left">
452
+ <div class="option-name">${escapeHtml(p.name)}</div>
453
+ <div class="option-credits">${formatCredits(p.credits)} credits</div>
454
+ </div>
455
+ <div class="option-price">${formatMoney(p.price, currency)}/${PERIOD_SUFFIX[p.period] ?? p.period}</div>
433
456
  </button>`
434
457
  ).join("")}
435
458
  </div>` : ""}
436
- ${this.renderPromoCode(Boolean(pkgs?.hasActiveCheckoutPromotion))}
437
459
  <div class="list">
438
460
  ${packages.length ? packages.map(
439
461
  (p) => `
440
462
  <button class="option" data-kind="package" data-id="${escapeHtml(p.id)}">
441
- <span>${escapeHtml(p.name)}</span>
442
- <span class="meta">${formatMoney(p.price, currency)} \xB7 ${formatCredits(p.credits)} cr</span>
463
+ <div class="option-left">
464
+ <div class="option-name">${escapeHtml(p.name)}</div>
465
+ <div class="option-credits">${formatCredits(p.credits)} credits</div>
466
+ </div>
467
+ <div class="option-price">${formatMoney(p.price, currency)}</div>
443
468
  </button>`
444
469
  ).join("") : showCustom ? "" : legacyAmounts.map(
445
470
  (a) => `
@@ -454,16 +479,12 @@ var CrediballPaywallElement = class extends CrediballElement {
454
479
  <button class="custom-add" type="button">${escapeHtml(addButtonText)}</button>
455
480
  </div>
456
481
  <p class="custom-preview" hidden></p>` : ""}
482
+ ${this.renderPromoCode(Boolean(pkgs?.hasActiveCheckoutPromotion))}
457
483
  ${this.renderAutoTopup(packages, pkgs?.autoTopup, currency)}
458
484
  <p class="error" hidden></p>
459
485
  <button class="close" part="close" type="button">Not now</button>
460
486
  ${this.referral?.link ? `<div class="referral">
461
- <div class="referral-label">${escapeHtml(
462
- (this.getAttribute("referral-label") ?? "Refer a friend \xB7 earn {credits} credits").replace(
463
- "{credits}",
464
- formatCredits(this.referral.rewardCredits ?? 0)
465
- )
466
- )}</div>
487
+ <div class="referral-label">${escapeHtml(referralLabel)}</div>
467
488
  <div class="referral-row">
468
489
  <div class="link" title="${escapeHtml(this.referral.link)}">${escapeHtml(this.referral.link)}</div>
469
490
  <button class="referral-copy" type="button">${this.referralCopied ? "Copied!" : "Copy"}</button>
@@ -505,6 +526,24 @@ var CrediballPaywallElement = class extends CrediballElement {
505
526
  this.promoCodeOpen = true;
506
527
  this.render();
507
528
  });
529
+ const promoInput = this.shadow.querySelector(".promo-code-input");
530
+ const promoErrorEl = this.shadow.querySelector(".promo-code-error");
531
+ if (promoInput && promoErrorEl) {
532
+ let promoTouched = false;
533
+ const checkPromoFormat = () => {
534
+ const value = promoInput.value.trim();
535
+ const valid = !value || /^[A-Za-z0-9_-]+$/.test(value);
536
+ const showError2 = promoTouched && !valid;
537
+ promoInput.classList.toggle("invalid", showError2);
538
+ promoErrorEl.hidden = !showError2;
539
+ if (showError2) promoErrorEl.textContent = "Promo codes only contain letters, numbers, - and _.";
540
+ };
541
+ promoInput.addEventListener("blur", () => {
542
+ promoTouched = true;
543
+ checkPromoFormat();
544
+ });
545
+ promoInput.addEventListener("input", checkPromoFormat);
546
+ }
508
547
  if (showCustom) {
509
548
  const input = this.shadow.querySelector(".custom-input");
510
549
  const addBtn = this.shadow.querySelector(".custom-add");
@@ -572,8 +611,9 @@ var CrediballPaywallElement = class extends CrediballElement {
572
611
  * once the app has an active Checkout promotion (same as the custom-amount
573
612
  * field auto-following the dashboard's custom-topup config) — the
574
613
  * `allow-promo-code` attribute is only needed to force it on before you've
575
- * set one up. Not validated client-side an invalid code simply doesn't
576
- * grant a bonus when the top-up is processed. */
614
+ * set one up. Format-only client check (flags an obvious typo/paste
615
+ * mistake on blur, see wireUpPromoCode()) — whether the code is actually
616
+ * active/redeemable is only known server-side when a purchase is made. */
577
617
  renderPromoCode(hasActiveCheckoutPromotion) {
578
618
  if (!this.hasAttribute("allow-promo-code") && !hasActiveCheckoutPromotion) return "";
579
619
  const label = this.getAttribute("promo-code-label") ?? "Have a promo code?";
@@ -585,6 +625,7 @@ var CrediballPaywallElement = class extends CrediballElement {
585
625
  return `<div class="promo-code">
586
626
  <div class="promo-code-label">${escapeHtml(label)}</div>
587
627
  <input class="promo-code-input" placeholder="PROMOCODE" aria-label="Promo code" />
628
+ <p class="promo-code-error" hidden></p>
588
629
  </div>`;
589
630
  }
590
631
  /** The current promo code input value (uppercased, trimmed), or undefined
@@ -631,13 +631,24 @@ var Crediball = (() => {
631
631
  .card {
632
632
  width: 100%;
633
633
  max-width: 420px;
634
+ /* Cap to the overlay's own (viewport-correct, since it's fixed/inset:0)
635
+ content box and scroll internally past that \u2014 otherwise a host with
636
+ many optional sections enabled (balance, subscribe, promo code,
637
+ packages, custom amount, auto top-up, referral) can produce a card
638
+ taller than the mobile viewport, which the centered overlay then
639
+ clips top and bottom with no way to reach the rest. */
640
+ max-height: 100%;
641
+ overflow-y: auto;
642
+ -webkit-overflow-scrolling: touch;
643
+ overscroll-behavior: contain;
644
+ box-sizing: border-box;
634
645
  padding: 32px;
635
646
  border-radius: var(--crediball-radius-card, 18px);
636
647
  background: var(--crediball-canvas, #ffffff);
637
648
  color: var(--crediball-ink, #1d1d1f);
638
649
  }
639
650
  .balance-row { margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid var(--crediball-hairline, #e0e0e0); }
640
- .eyebrow { font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--crediball-ink-muted, #7a7a7a); margin-bottom: 4px; }
651
+ .eyebrow { font-size: 11px; font-weight: 600; letter-spacing: 0.06em; color: var(--crediball-ink-muted, #7a7a7a); margin-bottom: 4px; }
641
652
  .balance-value { font-size: 22px; font-weight: 600; }
642
653
  h3 { margin: 0; font-size: 24px; font-weight: 600; letter-spacing: -0.01em; }
643
654
  p.desc { margin: 8px 0 0; font-size: 14px; color: var(--crediball-ink-muted, #7a7a7a); }
@@ -650,14 +661,17 @@ var Crediball = (() => {
650
661
  display: flex;
651
662
  justify-content: space-between;
652
663
  align-items: center;
664
+ gap: 16px;
653
665
  background: var(--crediball-accent, #0066cc);
654
666
  color: var(--crediball-on-accent, #ffffff);
655
667
  font: inherit;
656
- font-size: 17px;
657
668
  padding: 11px 22px;
658
669
  border-radius: var(--crediball-radius, 9999px);
659
670
  }
660
- button.option .meta { opacity: 0.85; font-size: 14px; }
671
+ .option-left { display: flex; flex-direction: column; align-items: flex-start; flex: 1; min-width: 0; }
672
+ .option-name { font-size: 17px; font-weight: 600; line-height: 1.2; }
673
+ .option-credits { font-size: 13px; opacity: 0.85; line-height: 1.2; }
674
+ .option-price { font-size: 17px; font-weight: 600; white-space: nowrap; flex-shrink: 0; }
661
675
  .custom-row { display: flex; gap: 8px; align-items: center; margin-top: 12px; }
662
676
  .custom-row .sym { font-size: 14px; color: var(--crediball-ink-muted, #7a7a7a); }
663
677
  .custom-row input {
@@ -718,6 +732,8 @@ var Crediball = (() => {
718
732
  color: var(--crediball-ink, #1d1d1f);
719
733
  outline: none;
720
734
  }
735
+ .promo-code-input.invalid { border-color: var(--crediball-accent, #0066cc); }
736
+ .promo-code-error { margin: 6px 0 0; font-size: 12px; color: var(--crediball-accent, #0066cc); }
721
737
  .autotopup { margin-top: 16px; }
722
738
  .autotopup-toggle {
723
739
  appearance: none; border: none; cursor: pointer; background: transparent; padding: 0;
@@ -740,7 +756,7 @@ var Crediball = (() => {
740
756
  }
741
757
  .autotopup-hint { margin: 6px 0 0; font-size: 11px; color: var(--crediball-ink-muted, #7a7a7a); }
742
758
  .referral { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--crediball-hairline, #e0e0e0); }
743
- .referral-label { margin-bottom: 8px; font-size: 12px; font-weight: 600; color: var(--crediball-ink-muted, #7a7a7a); }
759
+ .referral-label { margin-bottom: 8px; font-size: 12px; font-weight: 600; color: var(--crediball-ink-muted, #7a7a7a); white-space: pre-line; }
744
760
  .referral-row { display: flex; gap: 8px; align-items: center; }
745
761
  .referral-row .link {
746
762
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
@@ -828,6 +844,10 @@ var Crediball = (() => {
828
844
  const plans = pkgs?.activeSubscription ? [] : pkgs?.subscriptionPlans ?? [];
829
845
  const custom = pkgs?.customTopup;
830
846
  const showCustom = Boolean(custom?.enabled);
847
+ const referralReward = this.referral?.rewardCredits ?? 0;
848
+ const referredReward = this.referral?.referredRewardCredits ?? 0;
849
+ const defaultReferralLabel = referredReward > 0 ? "Refer a friend.\nYou earn {credits} credits and your friend {referredCredits} credits" : "Refer a friend \xB7 earn {credits} credits";
850
+ const referralLabel = (this.getAttribute("referral-label") ?? defaultReferralLabel).replace("{credits}", formatCredits(referralReward)).replace("{referredCredits}", formatCredits(referredReward));
831
851
  this.shadow.innerHTML = `
832
852
  <style>${STYLE}</style>
833
853
  <div class="overlay" part="overlay">
@@ -842,18 +862,23 @@ var Crediball = (() => {
842
862
  ${plans.map(
843
863
  (p) => `
844
864
  <button class="option" data-kind="plan" data-id="${escapeHtml(p.id)}">
845
- <span>${escapeHtml(p.name)}</span>
846
- <span class="meta">${formatMoney(p.price, currency)}/${PERIOD_SUFFIX[p.period] ?? p.period} \xB7 ${formatCredits(p.credits)} cr</span>
865
+ <div class="option-left">
866
+ <div class="option-name">${escapeHtml(p.name)}</div>
867
+ <div class="option-credits">${formatCredits(p.credits)} credits</div>
868
+ </div>
869
+ <div class="option-price">${formatMoney(p.price, currency)}/${PERIOD_SUFFIX[p.period] ?? p.period}</div>
847
870
  </button>`
848
871
  ).join("")}
849
872
  </div>` : ""}
850
- ${this.renderPromoCode(Boolean(pkgs?.hasActiveCheckoutPromotion))}
851
873
  <div class="list">
852
874
  ${packages.length ? packages.map(
853
875
  (p) => `
854
876
  <button class="option" data-kind="package" data-id="${escapeHtml(p.id)}">
855
- <span>${escapeHtml(p.name)}</span>
856
- <span class="meta">${formatMoney(p.price, currency)} \xB7 ${formatCredits(p.credits)} cr</span>
877
+ <div class="option-left">
878
+ <div class="option-name">${escapeHtml(p.name)}</div>
879
+ <div class="option-credits">${formatCredits(p.credits)} credits</div>
880
+ </div>
881
+ <div class="option-price">${formatMoney(p.price, currency)}</div>
857
882
  </button>`
858
883
  ).join("") : showCustom ? "" : legacyAmounts.map(
859
884
  (a) => `
@@ -868,16 +893,12 @@ var Crediball = (() => {
868
893
  <button class="custom-add" type="button">${escapeHtml(addButtonText)}</button>
869
894
  </div>
870
895
  <p class="custom-preview" hidden></p>` : ""}
896
+ ${this.renderPromoCode(Boolean(pkgs?.hasActiveCheckoutPromotion))}
871
897
  ${this.renderAutoTopup(packages, pkgs?.autoTopup, currency)}
872
898
  <p class="error" hidden></p>
873
899
  <button class="close" part="close" type="button">Not now</button>
874
900
  ${this.referral?.link ? `<div class="referral">
875
- <div class="referral-label">${escapeHtml(
876
- (this.getAttribute("referral-label") ?? "Refer a friend \xB7 earn {credits} credits").replace(
877
- "{credits}",
878
- formatCredits(this.referral.rewardCredits ?? 0)
879
- )
880
- )}</div>
901
+ <div class="referral-label">${escapeHtml(referralLabel)}</div>
881
902
  <div class="referral-row">
882
903
  <div class="link" title="${escapeHtml(this.referral.link)}">${escapeHtml(this.referral.link)}</div>
883
904
  <button class="referral-copy" type="button">${this.referralCopied ? "Copied!" : "Copy"}</button>
@@ -919,6 +940,24 @@ var Crediball = (() => {
919
940
  this.promoCodeOpen = true;
920
941
  this.render();
921
942
  });
943
+ const promoInput = this.shadow.querySelector(".promo-code-input");
944
+ const promoErrorEl = this.shadow.querySelector(".promo-code-error");
945
+ if (promoInput && promoErrorEl) {
946
+ let promoTouched = false;
947
+ const checkPromoFormat = () => {
948
+ const value = promoInput.value.trim();
949
+ const valid = !value || /^[A-Za-z0-9_-]+$/.test(value);
950
+ const showError2 = promoTouched && !valid;
951
+ promoInput.classList.toggle("invalid", showError2);
952
+ promoErrorEl.hidden = !showError2;
953
+ if (showError2) promoErrorEl.textContent = "Promo codes only contain letters, numbers, - and _.";
954
+ };
955
+ promoInput.addEventListener("blur", () => {
956
+ promoTouched = true;
957
+ checkPromoFormat();
958
+ });
959
+ promoInput.addEventListener("input", checkPromoFormat);
960
+ }
922
961
  if (showCustom) {
923
962
  const input = this.shadow.querySelector(".custom-input");
924
963
  const addBtn = this.shadow.querySelector(".custom-add");
@@ -986,8 +1025,9 @@ var Crediball = (() => {
986
1025
  * once the app has an active Checkout promotion (same as the custom-amount
987
1026
  * field auto-following the dashboard's custom-topup config) — the
988
1027
  * `allow-promo-code` attribute is only needed to force it on before you've
989
- * set one up. Not validated client-side an invalid code simply doesn't
990
- * grant a bonus when the top-up is processed. */
1028
+ * set one up. Format-only client check (flags an obvious typo/paste
1029
+ * mistake on blur, see wireUpPromoCode()) — whether the code is actually
1030
+ * active/redeemable is only known server-side when a purchase is made. */
991
1031
  renderPromoCode(hasActiveCheckoutPromotion) {
992
1032
  if (!this.hasAttribute("allow-promo-code") && !hasActiveCheckoutPromotion) return "";
993
1033
  const label = this.getAttribute("promo-code-label") ?? "Have a promo code?";
@@ -999,6 +1039,7 @@ var Crediball = (() => {
999
1039
  return `<div class="promo-code">
1000
1040
  <div class="promo-code-label">${escapeHtml(label)}</div>
1001
1041
  <input class="promo-code-input" placeholder="PROMOCODE" aria-label="Promo code" />
1042
+ <p class="promo-code-error" hidden></p>
1002
1043
  </div>`;
1003
1044
  }
1004
1045
  /** The current promo code input value (uppercased, trimmed), or undefined
package/dist/index.d.ts CHANGED
@@ -127,8 +127,9 @@ declare class CrediballPaywallElement extends CrediballElement {
127
127
  * once the app has an active Checkout promotion (same as the custom-amount
128
128
  * field auto-following the dashboard's custom-topup config) — the
129
129
  * `allow-promo-code` attribute is only needed to force it on before you've
130
- * set one up. Not validated client-side an invalid code simply doesn't
131
- * grant a bonus when the top-up is processed. */
130
+ * set one up. Format-only client check (flags an obvious typo/paste
131
+ * mistake on blur, see wireUpPromoCode()) — whether the code is actually
132
+ * active/redeemable is only known server-side when a purchase is made. */
132
133
  private renderPromoCode;
133
134
  /** The current promo code input value (uppercased, trimmed), or undefined
134
135
  * when the field isn't shown/filled in. Read at click time — never re-render
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  CrediballReferralCardElement,
7
7
  CrediballTopUpButtonElement,
8
8
  CrediballUsageElement
9
- } from "./chunk-YP5JNDTD.js";
9
+ } from "./chunk-FOMDJR2B.js";
10
10
  export {
11
11
  CrediballBalanceElement,
12
12
  CrediballElement,
package/dist/register.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  CrediballReferralCardElement,
6
6
  CrediballTopUpButtonElement,
7
7
  CrediballUsageElement
8
- } from "./chunk-YP5JNDTD.js";
8
+ } from "./chunk-FOMDJR2B.js";
9
9
 
10
10
  // src/register.ts
11
11
  function define(name, ctor) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crediball/elements",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Framework-agnostic web components for showing Crediball credits inside your AI app.",
5
5
  "license": "MIT",
6
6
  "author": "Filippo Rezzadore <filipporezzadore@gmail.com>",