@crediball/elements 0.10.0 → 0.12.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.
- package/dist/{chunk-YP5JNDTD.js → chunk-ELPD754U.js} +90 -21
- package/dist/crediball-elements.iife.js +112 -23
- package/dist/index.d.ts +14 -3
- package/dist/index.js +1 -1
- package/dist/register.js +1 -1
- package/package.json +2 -2
|
@@ -2,22 +2,33 @@
|
|
|
2
2
|
import {
|
|
3
3
|
getCrediballClient,
|
|
4
4
|
loadRemoteFont,
|
|
5
|
-
themeToCssVars
|
|
5
|
+
themeToCssVars,
|
|
6
|
+
getSystemColorScheme,
|
|
7
|
+
watchSystemColorScheme
|
|
6
8
|
} from "@crediball/core";
|
|
7
9
|
var _CrediballElement = class _CrediballElement extends HTMLElement {
|
|
8
10
|
constructor() {
|
|
9
11
|
super(...arguments);
|
|
10
12
|
this.client = null;
|
|
11
13
|
this.unsubscribe = null;
|
|
14
|
+
this.unwatchSystemColorScheme = null;
|
|
12
15
|
}
|
|
13
16
|
static get observedAttributes() {
|
|
14
|
-
return [..._CrediballElement.CLIENT_ATTRS];
|
|
17
|
+
return [..._CrediballElement.CLIENT_ATTRS, "color-scheme"];
|
|
15
18
|
}
|
|
16
19
|
connectedCallback() {
|
|
17
20
|
this.connectClient();
|
|
21
|
+
this.unwatchSystemColorScheme = watchSystemColorScheme(() => {
|
|
22
|
+
if (!this.explicitColorScheme()) {
|
|
23
|
+
this.applyRemoteTheme();
|
|
24
|
+
this.render();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
18
27
|
}
|
|
19
28
|
disconnectedCallback() {
|
|
20
29
|
this.teardownClient();
|
|
30
|
+
this.unwatchSystemColorScheme?.();
|
|
31
|
+
this.unwatchSystemColorScheme = null;
|
|
21
32
|
}
|
|
22
33
|
/**
|
|
23
34
|
* Only reconnect the shared client when a connection-relevant attribute
|
|
@@ -29,10 +40,21 @@ var _CrediballElement = class _CrediballElement extends HTMLElement {
|
|
|
29
40
|
if (!this.isConnected) return;
|
|
30
41
|
if (_CrediballElement.CLIENT_ATTRS.has(name)) {
|
|
31
42
|
this.connectClient();
|
|
43
|
+
} else if (name === "color-scheme") {
|
|
44
|
+
this.applyRemoteTheme();
|
|
45
|
+
this.render();
|
|
32
46
|
} else {
|
|
33
47
|
this.render();
|
|
34
48
|
}
|
|
35
49
|
}
|
|
50
|
+
explicitColorScheme() {
|
|
51
|
+
const attr = this.getAttribute("color-scheme");
|
|
52
|
+
return attr === "light" || attr === "dark" ? attr : null;
|
|
53
|
+
}
|
|
54
|
+
/** "light"/"dark" attribute wins; otherwise follows the live OS/browser preference. */
|
|
55
|
+
resolvedColorScheme() {
|
|
56
|
+
return this.explicitColorScheme() ?? getSystemColorScheme();
|
|
57
|
+
}
|
|
36
58
|
connectClient() {
|
|
37
59
|
this.teardownClient();
|
|
38
60
|
const publishableKey = this.getAttribute("publishable-key");
|
|
@@ -59,12 +81,18 @@ var _CrediballElement = class _CrediballElement extends HTMLElement {
|
|
|
59
81
|
* the shadow boundary, so the shadow-DOM styles pick them up. On by default;
|
|
60
82
|
* set `apply-remote-theme="false"` to theme from your own CSS instead. The
|
|
61
83
|
* published theme takes precedence over host `--crediball-*` vars (dashboard wins).
|
|
84
|
+
*
|
|
85
|
+
* If a dark palette is configured on the dashboard, it's applied whenever
|
|
86
|
+
* the resolved color scheme is "dark" — following the OS/browser's
|
|
87
|
+
* `prefers-color-scheme` live, or a `color-scheme="light"|"dark"` attribute
|
|
88
|
+
* you set explicitly (wins over the OS setting, for an app with its own
|
|
89
|
+
* in-app theme toggle).
|
|
62
90
|
*/
|
|
63
91
|
applyRemoteTheme() {
|
|
64
92
|
if (this.getAttribute("apply-remote-theme") === "false") return;
|
|
65
93
|
const theme = this.getSnapshot()?.packages?.ui?.theme;
|
|
66
94
|
if (!theme) return;
|
|
67
|
-
for (const [cssVar, value] of Object.entries(themeToCssVars(theme))) {
|
|
95
|
+
for (const [cssVar, value] of Object.entries(themeToCssVars(theme, this.resolvedColorScheme()))) {
|
|
68
96
|
this.style.setProperty(cssVar, value);
|
|
69
97
|
}
|
|
70
98
|
if (theme.fontUrl) loadRemoteFont(theme.fontUrl);
|
|
@@ -217,13 +245,24 @@ var STYLE = `
|
|
|
217
245
|
.card {
|
|
218
246
|
width: 100%;
|
|
219
247
|
max-width: 420px;
|
|
248
|
+
/* Cap to the overlay's own (viewport-correct, since it's fixed/inset:0)
|
|
249
|
+
content box and scroll internally past that \u2014 otherwise a host with
|
|
250
|
+
many optional sections enabled (balance, subscribe, promo code,
|
|
251
|
+
packages, custom amount, auto top-up, referral) can produce a card
|
|
252
|
+
taller than the mobile viewport, which the centered overlay then
|
|
253
|
+
clips top and bottom with no way to reach the rest. */
|
|
254
|
+
max-height: 100%;
|
|
255
|
+
overflow-y: auto;
|
|
256
|
+
-webkit-overflow-scrolling: touch;
|
|
257
|
+
overscroll-behavior: contain;
|
|
258
|
+
box-sizing: border-box;
|
|
220
259
|
padding: 32px;
|
|
221
260
|
border-radius: var(--crediball-radius-card, 18px);
|
|
222
261
|
background: var(--crediball-canvas, #ffffff);
|
|
223
262
|
color: var(--crediball-ink, #1d1d1f);
|
|
224
263
|
}
|
|
225
264
|
.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;
|
|
265
|
+
.eyebrow { font-size: 11px; font-weight: 600; letter-spacing: 0.06em; color: var(--crediball-ink-muted, #7a7a7a); margin-bottom: 4px; }
|
|
227
266
|
.balance-value { font-size: 22px; font-weight: 600; }
|
|
228
267
|
h3 { margin: 0; font-size: 24px; font-weight: 600; letter-spacing: -0.01em; }
|
|
229
268
|
p.desc { margin: 8px 0 0; font-size: 14px; color: var(--crediball-ink-muted, #7a7a7a); }
|
|
@@ -236,14 +275,17 @@ var STYLE = `
|
|
|
236
275
|
display: flex;
|
|
237
276
|
justify-content: space-between;
|
|
238
277
|
align-items: center;
|
|
278
|
+
gap: 16px;
|
|
239
279
|
background: var(--crediball-accent, #0066cc);
|
|
240
280
|
color: var(--crediball-on-accent, #ffffff);
|
|
241
281
|
font: inherit;
|
|
242
|
-
font-size: 17px;
|
|
243
282
|
padding: 11px 22px;
|
|
244
283
|
border-radius: var(--crediball-radius, 9999px);
|
|
245
284
|
}
|
|
246
|
-
|
|
285
|
+
.option-left { display: flex; flex-direction: column; align-items: flex-start; flex: 1; min-width: 0; }
|
|
286
|
+
.option-name { font-size: 17px; font-weight: 600; line-height: 1.2; }
|
|
287
|
+
.option-credits { font-size: 13px; opacity: 0.85; line-height: 1.2; }
|
|
288
|
+
.option-price { font-size: 17px; font-weight: 600; white-space: nowrap; flex-shrink: 0; }
|
|
247
289
|
.custom-row { display: flex; gap: 8px; align-items: center; margin-top: 12px; }
|
|
248
290
|
.custom-row .sym { font-size: 14px; color: var(--crediball-ink-muted, #7a7a7a); }
|
|
249
291
|
.custom-row input {
|
|
@@ -304,6 +346,8 @@ var STYLE = `
|
|
|
304
346
|
color: var(--crediball-ink, #1d1d1f);
|
|
305
347
|
outline: none;
|
|
306
348
|
}
|
|
349
|
+
.promo-code-input.invalid { border-color: var(--crediball-accent, #0066cc); }
|
|
350
|
+
.promo-code-error { margin: 6px 0 0; font-size: 12px; color: var(--crediball-accent, #0066cc); }
|
|
307
351
|
.autotopup { margin-top: 16px; }
|
|
308
352
|
.autotopup-toggle {
|
|
309
353
|
appearance: none; border: none; cursor: pointer; background: transparent; padding: 0;
|
|
@@ -326,7 +370,7 @@ var STYLE = `
|
|
|
326
370
|
}
|
|
327
371
|
.autotopup-hint { margin: 6px 0 0; font-size: 11px; color: var(--crediball-ink-muted, #7a7a7a); }
|
|
328
372
|
.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); }
|
|
373
|
+
.referral-label { margin-bottom: 8px; font-size: 12px; font-weight: 600; color: var(--crediball-ink-muted, #7a7a7a); white-space: pre-line; }
|
|
330
374
|
.referral-row { display: flex; gap: 8px; align-items: center; }
|
|
331
375
|
.referral-row .link {
|
|
332
376
|
flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
@@ -414,6 +458,10 @@ var CrediballPaywallElement = class extends CrediballElement {
|
|
|
414
458
|
const plans = pkgs?.activeSubscription ? [] : pkgs?.subscriptionPlans ?? [];
|
|
415
459
|
const custom = pkgs?.customTopup;
|
|
416
460
|
const showCustom = Boolean(custom?.enabled);
|
|
461
|
+
const referralReward = this.referral?.rewardCredits ?? 0;
|
|
462
|
+
const referredReward = this.referral?.referredRewardCredits ?? 0;
|
|
463
|
+
const defaultReferralLabel = referredReward > 0 ? "Refer a friend.\nYou earn {credits} credits and your friend {referredCredits} credits" : "Refer a friend \xB7 earn {credits} credits";
|
|
464
|
+
const referralLabel = (this.getAttribute("referral-label") ?? defaultReferralLabel).replace("{credits}", formatCredits(referralReward)).replace("{referredCredits}", formatCredits(referredReward));
|
|
417
465
|
this.shadow.innerHTML = `
|
|
418
466
|
<style>${STYLE}</style>
|
|
419
467
|
<div class="overlay" part="overlay">
|
|
@@ -428,18 +476,23 @@ var CrediballPaywallElement = class extends CrediballElement {
|
|
|
428
476
|
${plans.map(
|
|
429
477
|
(p) => `
|
|
430
478
|
<button class="option" data-kind="plan" data-id="${escapeHtml(p.id)}">
|
|
431
|
-
<
|
|
432
|
-
|
|
479
|
+
<div class="option-left">
|
|
480
|
+
<div class="option-name">${escapeHtml(p.name)}</div>
|
|
481
|
+
<div class="option-credits">${formatCredits(p.credits)} credits</div>
|
|
482
|
+
</div>
|
|
483
|
+
<div class="option-price">${formatMoney(p.price, currency)}/${PERIOD_SUFFIX[p.period] ?? p.period}</div>
|
|
433
484
|
</button>`
|
|
434
485
|
).join("")}
|
|
435
486
|
</div>` : ""}
|
|
436
|
-
${this.renderPromoCode(Boolean(pkgs?.hasActiveCheckoutPromotion))}
|
|
437
487
|
<div class="list">
|
|
438
488
|
${packages.length ? packages.map(
|
|
439
489
|
(p) => `
|
|
440
490
|
<button class="option" data-kind="package" data-id="${escapeHtml(p.id)}">
|
|
441
|
-
<
|
|
442
|
-
|
|
491
|
+
<div class="option-left">
|
|
492
|
+
<div class="option-name">${escapeHtml(p.name)}</div>
|
|
493
|
+
<div class="option-credits">${formatCredits(p.credits)} credits</div>
|
|
494
|
+
</div>
|
|
495
|
+
<div class="option-price">${formatMoney(p.price, currency)}</div>
|
|
443
496
|
</button>`
|
|
444
497
|
).join("") : showCustom ? "" : legacyAmounts.map(
|
|
445
498
|
(a) => `
|
|
@@ -454,16 +507,12 @@ var CrediballPaywallElement = class extends CrediballElement {
|
|
|
454
507
|
<button class="custom-add" type="button">${escapeHtml(addButtonText)}</button>
|
|
455
508
|
</div>
|
|
456
509
|
<p class="custom-preview" hidden></p>` : ""}
|
|
457
|
-
${this.
|
|
510
|
+
${this.renderPromoCode(Boolean(pkgs?.hasActiveCheckoutPromotion))}
|
|
458
511
|
<p class="error" hidden></p>
|
|
512
|
+
${this.renderAutoTopup(packages, pkgs?.autoTopup, currency)}
|
|
459
513
|
<button class="close" part="close" type="button">Not now</button>
|
|
460
514
|
${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>
|
|
515
|
+
<div class="referral-label">${escapeHtml(referralLabel)}</div>
|
|
467
516
|
<div class="referral-row">
|
|
468
517
|
<div class="link" title="${escapeHtml(this.referral.link)}">${escapeHtml(this.referral.link)}</div>
|
|
469
518
|
<button class="referral-copy" type="button">${this.referralCopied ? "Copied!" : "Copy"}</button>
|
|
@@ -505,6 +554,24 @@ var CrediballPaywallElement = class extends CrediballElement {
|
|
|
505
554
|
this.promoCodeOpen = true;
|
|
506
555
|
this.render();
|
|
507
556
|
});
|
|
557
|
+
const promoInput = this.shadow.querySelector(".promo-code-input");
|
|
558
|
+
const promoErrorEl = this.shadow.querySelector(".promo-code-error");
|
|
559
|
+
if (promoInput && promoErrorEl) {
|
|
560
|
+
let promoTouched = false;
|
|
561
|
+
const checkPromoFormat = () => {
|
|
562
|
+
const value = promoInput.value.trim();
|
|
563
|
+
const valid = !value || /^[A-Za-z0-9_-]+$/.test(value);
|
|
564
|
+
const showError2 = promoTouched && !valid;
|
|
565
|
+
promoInput.classList.toggle("invalid", showError2);
|
|
566
|
+
promoErrorEl.hidden = !showError2;
|
|
567
|
+
if (showError2) promoErrorEl.textContent = "Promo codes only contain letters, numbers, - and _.";
|
|
568
|
+
};
|
|
569
|
+
promoInput.addEventListener("blur", () => {
|
|
570
|
+
promoTouched = true;
|
|
571
|
+
checkPromoFormat();
|
|
572
|
+
});
|
|
573
|
+
promoInput.addEventListener("input", checkPromoFormat);
|
|
574
|
+
}
|
|
508
575
|
if (showCustom) {
|
|
509
576
|
const input = this.shadow.querySelector(".custom-input");
|
|
510
577
|
const addBtn = this.shadow.querySelector(".custom-add");
|
|
@@ -572,8 +639,9 @@ var CrediballPaywallElement = class extends CrediballElement {
|
|
|
572
639
|
* once the app has an active Checkout promotion (same as the custom-amount
|
|
573
640
|
* field auto-following the dashboard's custom-topup config) — the
|
|
574
641
|
* `allow-promo-code` attribute is only needed to force it on before you've
|
|
575
|
-
* set one up.
|
|
576
|
-
*
|
|
642
|
+
* set one up. Format-only client check (flags an obvious typo/paste
|
|
643
|
+
* mistake on blur, see wireUpPromoCode()) — whether the code is actually
|
|
644
|
+
* active/redeemable is only known server-side when a purchase is made. */
|
|
577
645
|
renderPromoCode(hasActiveCheckoutPromotion) {
|
|
578
646
|
if (!this.hasAttribute("allow-promo-code") && !hasActiveCheckoutPromotion) return "";
|
|
579
647
|
const label = this.getAttribute("promo-code-label") ?? "Have a promo code?";
|
|
@@ -585,6 +653,7 @@ var CrediballPaywallElement = class extends CrediballElement {
|
|
|
585
653
|
return `<div class="promo-code">
|
|
586
654
|
<div class="promo-code-label">${escapeHtml(label)}</div>
|
|
587
655
|
<input class="promo-code-input" placeholder="PROMOCODE" aria-label="Promo code" />
|
|
656
|
+
<p class="promo-code-error" hidden></p>
|
|
588
657
|
</div>`;
|
|
589
658
|
}
|
|
590
659
|
/** The current promo code input value (uppercased, trimmed), or undefined
|
|
@@ -384,6 +384,27 @@ var Crediball = (() => {
|
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
// ../core/dist/theme.js
|
|
387
|
+
function getSystemColorScheme() {
|
|
388
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function")
|
|
389
|
+
return "light";
|
|
390
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
391
|
+
}
|
|
392
|
+
function watchSystemColorScheme(cb) {
|
|
393
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function")
|
|
394
|
+
return () => {
|
|
395
|
+
};
|
|
396
|
+
const mql = window.matchMedia("(prefers-color-scheme: dark)");
|
|
397
|
+
const handler = () => cb(mql.matches ? "dark" : "light");
|
|
398
|
+
mql.addEventListener("change", handler);
|
|
399
|
+
return () => mql.removeEventListener("change", handler);
|
|
400
|
+
}
|
|
401
|
+
function resolveUiTheme(theme, scheme) {
|
|
402
|
+
if (!theme)
|
|
403
|
+
return theme ?? null;
|
|
404
|
+
if (scheme !== "dark" || !theme.dark)
|
|
405
|
+
return theme;
|
|
406
|
+
return { ...theme, ...theme.dark };
|
|
407
|
+
}
|
|
387
408
|
var THEME_CSS_VAR_ENTRIES = [
|
|
388
409
|
["accent", "--crediball-accent", String],
|
|
389
410
|
["onAccent", "--crediball-on-accent", String],
|
|
@@ -394,12 +415,13 @@ var Crediball = (() => {
|
|
|
394
415
|
["radiusCard", "--crediball-radius-card", (v) => `${v}px`],
|
|
395
416
|
["fontStack", "--crediball-font", String]
|
|
396
417
|
];
|
|
397
|
-
function themeToCssVars(theme) {
|
|
418
|
+
function themeToCssVars(theme, scheme = "light") {
|
|
398
419
|
const vars = {};
|
|
399
|
-
|
|
420
|
+
const resolved = resolveUiTheme(theme, scheme);
|
|
421
|
+
if (!resolved)
|
|
400
422
|
return vars;
|
|
401
423
|
for (const [key, cssVar, fmt] of THEME_CSS_VAR_ENTRIES) {
|
|
402
|
-
const value =
|
|
424
|
+
const value = resolved[key];
|
|
403
425
|
if (value != null && value !== "")
|
|
404
426
|
vars[cssVar] = fmt(value);
|
|
405
427
|
}
|
|
@@ -428,15 +450,24 @@ var Crediball = (() => {
|
|
|
428
450
|
super(...arguments);
|
|
429
451
|
this.client = null;
|
|
430
452
|
this.unsubscribe = null;
|
|
453
|
+
this.unwatchSystemColorScheme = null;
|
|
431
454
|
}
|
|
432
455
|
static get observedAttributes() {
|
|
433
|
-
return [..._CrediballElement.CLIENT_ATTRS];
|
|
456
|
+
return [..._CrediballElement.CLIENT_ATTRS, "color-scheme"];
|
|
434
457
|
}
|
|
435
458
|
connectedCallback() {
|
|
436
459
|
this.connectClient();
|
|
460
|
+
this.unwatchSystemColorScheme = watchSystemColorScheme(() => {
|
|
461
|
+
if (!this.explicitColorScheme()) {
|
|
462
|
+
this.applyRemoteTheme();
|
|
463
|
+
this.render();
|
|
464
|
+
}
|
|
465
|
+
});
|
|
437
466
|
}
|
|
438
467
|
disconnectedCallback() {
|
|
439
468
|
this.teardownClient();
|
|
469
|
+
this.unwatchSystemColorScheme?.();
|
|
470
|
+
this.unwatchSystemColorScheme = null;
|
|
440
471
|
}
|
|
441
472
|
/**
|
|
442
473
|
* Only reconnect the shared client when a connection-relevant attribute
|
|
@@ -448,10 +479,21 @@ var Crediball = (() => {
|
|
|
448
479
|
if (!this.isConnected) return;
|
|
449
480
|
if (_CrediballElement.CLIENT_ATTRS.has(name)) {
|
|
450
481
|
this.connectClient();
|
|
482
|
+
} else if (name === "color-scheme") {
|
|
483
|
+
this.applyRemoteTheme();
|
|
484
|
+
this.render();
|
|
451
485
|
} else {
|
|
452
486
|
this.render();
|
|
453
487
|
}
|
|
454
488
|
}
|
|
489
|
+
explicitColorScheme() {
|
|
490
|
+
const attr = this.getAttribute("color-scheme");
|
|
491
|
+
return attr === "light" || attr === "dark" ? attr : null;
|
|
492
|
+
}
|
|
493
|
+
/** "light"/"dark" attribute wins; otherwise follows the live OS/browser preference. */
|
|
494
|
+
resolvedColorScheme() {
|
|
495
|
+
return this.explicitColorScheme() ?? getSystemColorScheme();
|
|
496
|
+
}
|
|
455
497
|
connectClient() {
|
|
456
498
|
this.teardownClient();
|
|
457
499
|
const publishableKey = this.getAttribute("publishable-key");
|
|
@@ -478,12 +520,18 @@ var Crediball = (() => {
|
|
|
478
520
|
* the shadow boundary, so the shadow-DOM styles pick them up. On by default;
|
|
479
521
|
* set `apply-remote-theme="false"` to theme from your own CSS instead. The
|
|
480
522
|
* published theme takes precedence over host `--crediball-*` vars (dashboard wins).
|
|
523
|
+
*
|
|
524
|
+
* If a dark palette is configured on the dashboard, it's applied whenever
|
|
525
|
+
* the resolved color scheme is "dark" — following the OS/browser's
|
|
526
|
+
* `prefers-color-scheme` live, or a `color-scheme="light"|"dark"` attribute
|
|
527
|
+
* you set explicitly (wins over the OS setting, for an app with its own
|
|
528
|
+
* in-app theme toggle).
|
|
481
529
|
*/
|
|
482
530
|
applyRemoteTheme() {
|
|
483
531
|
if (this.getAttribute("apply-remote-theme") === "false") return;
|
|
484
532
|
const theme = this.getSnapshot()?.packages?.ui?.theme;
|
|
485
533
|
if (!theme) return;
|
|
486
|
-
for (const [cssVar, value] of Object.entries(themeToCssVars(theme))) {
|
|
534
|
+
for (const [cssVar, value] of Object.entries(themeToCssVars(theme, this.resolvedColorScheme()))) {
|
|
487
535
|
this.style.setProperty(cssVar, value);
|
|
488
536
|
}
|
|
489
537
|
if (theme.fontUrl) loadRemoteFont(theme.fontUrl);
|
|
@@ -631,13 +679,24 @@ var Crediball = (() => {
|
|
|
631
679
|
.card {
|
|
632
680
|
width: 100%;
|
|
633
681
|
max-width: 420px;
|
|
682
|
+
/* Cap to the overlay's own (viewport-correct, since it's fixed/inset:0)
|
|
683
|
+
content box and scroll internally past that \u2014 otherwise a host with
|
|
684
|
+
many optional sections enabled (balance, subscribe, promo code,
|
|
685
|
+
packages, custom amount, auto top-up, referral) can produce a card
|
|
686
|
+
taller than the mobile viewport, which the centered overlay then
|
|
687
|
+
clips top and bottom with no way to reach the rest. */
|
|
688
|
+
max-height: 100%;
|
|
689
|
+
overflow-y: auto;
|
|
690
|
+
-webkit-overflow-scrolling: touch;
|
|
691
|
+
overscroll-behavior: contain;
|
|
692
|
+
box-sizing: border-box;
|
|
634
693
|
padding: 32px;
|
|
635
694
|
border-radius: var(--crediball-radius-card, 18px);
|
|
636
695
|
background: var(--crediball-canvas, #ffffff);
|
|
637
696
|
color: var(--crediball-ink, #1d1d1f);
|
|
638
697
|
}
|
|
639
698
|
.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;
|
|
699
|
+
.eyebrow { font-size: 11px; font-weight: 600; letter-spacing: 0.06em; color: var(--crediball-ink-muted, #7a7a7a); margin-bottom: 4px; }
|
|
641
700
|
.balance-value { font-size: 22px; font-weight: 600; }
|
|
642
701
|
h3 { margin: 0; font-size: 24px; font-weight: 600; letter-spacing: -0.01em; }
|
|
643
702
|
p.desc { margin: 8px 0 0; font-size: 14px; color: var(--crediball-ink-muted, #7a7a7a); }
|
|
@@ -650,14 +709,17 @@ var Crediball = (() => {
|
|
|
650
709
|
display: flex;
|
|
651
710
|
justify-content: space-between;
|
|
652
711
|
align-items: center;
|
|
712
|
+
gap: 16px;
|
|
653
713
|
background: var(--crediball-accent, #0066cc);
|
|
654
714
|
color: var(--crediball-on-accent, #ffffff);
|
|
655
715
|
font: inherit;
|
|
656
|
-
font-size: 17px;
|
|
657
716
|
padding: 11px 22px;
|
|
658
717
|
border-radius: var(--crediball-radius, 9999px);
|
|
659
718
|
}
|
|
660
|
-
|
|
719
|
+
.option-left { display: flex; flex-direction: column; align-items: flex-start; flex: 1; min-width: 0; }
|
|
720
|
+
.option-name { font-size: 17px; font-weight: 600; line-height: 1.2; }
|
|
721
|
+
.option-credits { font-size: 13px; opacity: 0.85; line-height: 1.2; }
|
|
722
|
+
.option-price { font-size: 17px; font-weight: 600; white-space: nowrap; flex-shrink: 0; }
|
|
661
723
|
.custom-row { display: flex; gap: 8px; align-items: center; margin-top: 12px; }
|
|
662
724
|
.custom-row .sym { font-size: 14px; color: var(--crediball-ink-muted, #7a7a7a); }
|
|
663
725
|
.custom-row input {
|
|
@@ -718,6 +780,8 @@ var Crediball = (() => {
|
|
|
718
780
|
color: var(--crediball-ink, #1d1d1f);
|
|
719
781
|
outline: none;
|
|
720
782
|
}
|
|
783
|
+
.promo-code-input.invalid { border-color: var(--crediball-accent, #0066cc); }
|
|
784
|
+
.promo-code-error { margin: 6px 0 0; font-size: 12px; color: var(--crediball-accent, #0066cc); }
|
|
721
785
|
.autotopup { margin-top: 16px; }
|
|
722
786
|
.autotopup-toggle {
|
|
723
787
|
appearance: none; border: none; cursor: pointer; background: transparent; padding: 0;
|
|
@@ -740,7 +804,7 @@ var Crediball = (() => {
|
|
|
740
804
|
}
|
|
741
805
|
.autotopup-hint { margin: 6px 0 0; font-size: 11px; color: var(--crediball-ink-muted, #7a7a7a); }
|
|
742
806
|
.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); }
|
|
807
|
+
.referral-label { margin-bottom: 8px; font-size: 12px; font-weight: 600; color: var(--crediball-ink-muted, #7a7a7a); white-space: pre-line; }
|
|
744
808
|
.referral-row { display: flex; gap: 8px; align-items: center; }
|
|
745
809
|
.referral-row .link {
|
|
746
810
|
flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
@@ -828,6 +892,10 @@ var Crediball = (() => {
|
|
|
828
892
|
const plans = pkgs?.activeSubscription ? [] : pkgs?.subscriptionPlans ?? [];
|
|
829
893
|
const custom = pkgs?.customTopup;
|
|
830
894
|
const showCustom = Boolean(custom?.enabled);
|
|
895
|
+
const referralReward = this.referral?.rewardCredits ?? 0;
|
|
896
|
+
const referredReward = this.referral?.referredRewardCredits ?? 0;
|
|
897
|
+
const defaultReferralLabel = referredReward > 0 ? "Refer a friend.\nYou earn {credits} credits and your friend {referredCredits} credits" : "Refer a friend \xB7 earn {credits} credits";
|
|
898
|
+
const referralLabel = (this.getAttribute("referral-label") ?? defaultReferralLabel).replace("{credits}", formatCredits(referralReward)).replace("{referredCredits}", formatCredits(referredReward));
|
|
831
899
|
this.shadow.innerHTML = `
|
|
832
900
|
<style>${STYLE}</style>
|
|
833
901
|
<div class="overlay" part="overlay">
|
|
@@ -842,18 +910,23 @@ var Crediball = (() => {
|
|
|
842
910
|
${plans.map(
|
|
843
911
|
(p) => `
|
|
844
912
|
<button class="option" data-kind="plan" data-id="${escapeHtml(p.id)}">
|
|
845
|
-
<
|
|
846
|
-
|
|
913
|
+
<div class="option-left">
|
|
914
|
+
<div class="option-name">${escapeHtml(p.name)}</div>
|
|
915
|
+
<div class="option-credits">${formatCredits(p.credits)} credits</div>
|
|
916
|
+
</div>
|
|
917
|
+
<div class="option-price">${formatMoney(p.price, currency)}/${PERIOD_SUFFIX[p.period] ?? p.period}</div>
|
|
847
918
|
</button>`
|
|
848
919
|
).join("")}
|
|
849
920
|
</div>` : ""}
|
|
850
|
-
${this.renderPromoCode(Boolean(pkgs?.hasActiveCheckoutPromotion))}
|
|
851
921
|
<div class="list">
|
|
852
922
|
${packages.length ? packages.map(
|
|
853
923
|
(p) => `
|
|
854
924
|
<button class="option" data-kind="package" data-id="${escapeHtml(p.id)}">
|
|
855
|
-
<
|
|
856
|
-
|
|
925
|
+
<div class="option-left">
|
|
926
|
+
<div class="option-name">${escapeHtml(p.name)}</div>
|
|
927
|
+
<div class="option-credits">${formatCredits(p.credits)} credits</div>
|
|
928
|
+
</div>
|
|
929
|
+
<div class="option-price">${formatMoney(p.price, currency)}</div>
|
|
857
930
|
</button>`
|
|
858
931
|
).join("") : showCustom ? "" : legacyAmounts.map(
|
|
859
932
|
(a) => `
|
|
@@ -868,16 +941,12 @@ var Crediball = (() => {
|
|
|
868
941
|
<button class="custom-add" type="button">${escapeHtml(addButtonText)}</button>
|
|
869
942
|
</div>
|
|
870
943
|
<p class="custom-preview" hidden></p>` : ""}
|
|
871
|
-
${this.
|
|
944
|
+
${this.renderPromoCode(Boolean(pkgs?.hasActiveCheckoutPromotion))}
|
|
872
945
|
<p class="error" hidden></p>
|
|
946
|
+
${this.renderAutoTopup(packages, pkgs?.autoTopup, currency)}
|
|
873
947
|
<button class="close" part="close" type="button">Not now</button>
|
|
874
948
|
${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>
|
|
949
|
+
<div class="referral-label">${escapeHtml(referralLabel)}</div>
|
|
881
950
|
<div class="referral-row">
|
|
882
951
|
<div class="link" title="${escapeHtml(this.referral.link)}">${escapeHtml(this.referral.link)}</div>
|
|
883
952
|
<button class="referral-copy" type="button">${this.referralCopied ? "Copied!" : "Copy"}</button>
|
|
@@ -919,6 +988,24 @@ var Crediball = (() => {
|
|
|
919
988
|
this.promoCodeOpen = true;
|
|
920
989
|
this.render();
|
|
921
990
|
});
|
|
991
|
+
const promoInput = this.shadow.querySelector(".promo-code-input");
|
|
992
|
+
const promoErrorEl = this.shadow.querySelector(".promo-code-error");
|
|
993
|
+
if (promoInput && promoErrorEl) {
|
|
994
|
+
let promoTouched = false;
|
|
995
|
+
const checkPromoFormat = () => {
|
|
996
|
+
const value = promoInput.value.trim();
|
|
997
|
+
const valid = !value || /^[A-Za-z0-9_-]+$/.test(value);
|
|
998
|
+
const showError2 = promoTouched && !valid;
|
|
999
|
+
promoInput.classList.toggle("invalid", showError2);
|
|
1000
|
+
promoErrorEl.hidden = !showError2;
|
|
1001
|
+
if (showError2) promoErrorEl.textContent = "Promo codes only contain letters, numbers, - and _.";
|
|
1002
|
+
};
|
|
1003
|
+
promoInput.addEventListener("blur", () => {
|
|
1004
|
+
promoTouched = true;
|
|
1005
|
+
checkPromoFormat();
|
|
1006
|
+
});
|
|
1007
|
+
promoInput.addEventListener("input", checkPromoFormat);
|
|
1008
|
+
}
|
|
922
1009
|
if (showCustom) {
|
|
923
1010
|
const input = this.shadow.querySelector(".custom-input");
|
|
924
1011
|
const addBtn = this.shadow.querySelector(".custom-add");
|
|
@@ -986,8 +1073,9 @@ var Crediball = (() => {
|
|
|
986
1073
|
* once the app has an active Checkout promotion (same as the custom-amount
|
|
987
1074
|
* field auto-following the dashboard's custom-topup config) — the
|
|
988
1075
|
* `allow-promo-code` attribute is only needed to force it on before you've
|
|
989
|
-
* set one up.
|
|
990
|
-
*
|
|
1076
|
+
* set one up. Format-only client check (flags an obvious typo/paste
|
|
1077
|
+
* mistake on blur, see wireUpPromoCode()) — whether the code is actually
|
|
1078
|
+
* active/redeemable is only known server-side when a purchase is made. */
|
|
991
1079
|
renderPromoCode(hasActiveCheckoutPromotion) {
|
|
992
1080
|
if (!this.hasAttribute("allow-promo-code") && !hasActiveCheckoutPromotion) return "";
|
|
993
1081
|
const label = this.getAttribute("promo-code-label") ?? "Have a promo code?";
|
|
@@ -999,6 +1087,7 @@ var Crediball = (() => {
|
|
|
999
1087
|
return `<div class="promo-code">
|
|
1000
1088
|
<div class="promo-code-label">${escapeHtml(label)}</div>
|
|
1001
1089
|
<input class="promo-code-input" placeholder="PROMOCODE" aria-label="Promo code" />
|
|
1090
|
+
<p class="promo-code-error" hidden></p>
|
|
1002
1091
|
</div>`;
|
|
1003
1092
|
}
|
|
1004
1093
|
/** The current promo code input value (uppercased, trimmed), or undefined
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CrediballClient, CrediballSnapshot } from '@crediball/core';
|
|
1
|
+
import { CrediballClient, ColorScheme, CrediballSnapshot } from '@crediball/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Shared base for every <crediball-*> element: reads config from attributes,
|
|
@@ -12,6 +12,7 @@ import { CrediballClient, CrediballSnapshot } from '@crediball/core';
|
|
|
12
12
|
declare abstract class CrediballElement extends HTMLElement {
|
|
13
13
|
protected client: CrediballClient | null;
|
|
14
14
|
private unsubscribe;
|
|
15
|
+
private unwatchSystemColorScheme;
|
|
15
16
|
private static readonly CLIENT_ATTRS;
|
|
16
17
|
static get observedAttributes(): string[];
|
|
17
18
|
connectedCallback(): void;
|
|
@@ -23,6 +24,9 @@ declare abstract class CrediballElement extends HTMLElement {
|
|
|
23
24
|
* those should just re-render, not tear down and refetch the client.
|
|
24
25
|
*/
|
|
25
26
|
attributeChangedCallback(name: string): void;
|
|
27
|
+
private explicitColorScheme;
|
|
28
|
+
/** "light"/"dark" attribute wins; otherwise follows the live OS/browser preference. */
|
|
29
|
+
protected resolvedColorScheme(): ColorScheme;
|
|
26
30
|
private connectClient;
|
|
27
31
|
/**
|
|
28
32
|
* Apply the developer-published theme (served alongside packages) as
|
|
@@ -30,6 +34,12 @@ declare abstract class CrediballElement extends HTMLElement {
|
|
|
30
34
|
* the shadow boundary, so the shadow-DOM styles pick them up. On by default;
|
|
31
35
|
* set `apply-remote-theme="false"` to theme from your own CSS instead. The
|
|
32
36
|
* published theme takes precedence over host `--crediball-*` vars (dashboard wins).
|
|
37
|
+
*
|
|
38
|
+
* If a dark palette is configured on the dashboard, it's applied whenever
|
|
39
|
+
* the resolved color scheme is "dark" — following the OS/browser's
|
|
40
|
+
* `prefers-color-scheme` live, or a `color-scheme="light"|"dark"` attribute
|
|
41
|
+
* you set explicitly (wins over the OS setting, for an app with its own
|
|
42
|
+
* in-app theme toggle).
|
|
33
43
|
*/
|
|
34
44
|
private applyRemoteTheme;
|
|
35
45
|
private teardownClient;
|
|
@@ -127,8 +137,9 @@ declare class CrediballPaywallElement extends CrediballElement {
|
|
|
127
137
|
* once the app has an active Checkout promotion (same as the custom-amount
|
|
128
138
|
* field auto-following the dashboard's custom-topup config) — the
|
|
129
139
|
* `allow-promo-code` attribute is only needed to force it on before you've
|
|
130
|
-
* set one up.
|
|
131
|
-
*
|
|
140
|
+
* set one up. Format-only client check (flags an obvious typo/paste
|
|
141
|
+
* mistake on blur, see wireUpPromoCode()) — whether the code is actually
|
|
142
|
+
* active/redeemable is only known server-side when a purchase is made. */
|
|
132
143
|
private renderPromoCode;
|
|
133
144
|
/** The current promo code input value (uppercased, trimmed), or undefined
|
|
134
145
|
* when the field isn't shown/filled in. Read at click time — never re-render
|
package/dist/index.js
CHANGED
package/dist/register.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crediball/elements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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>",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"prepublishOnly": "npm run build"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@crediball/core": "^0.
|
|
59
|
+
"@crediball/core": "^0.10.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"tsup": "^8.3.5",
|