@crediball/elements 0.3.0 → 0.4.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.
@@ -1,5 +1,9 @@
1
1
  // src/base.ts
2
- import { getCrediballClient } from "@crediball/core";
2
+ import {
3
+ getCrediballClient,
4
+ loadRemoteFont,
5
+ themeToCssVars
6
+ } from "@crediball/core";
3
7
  var _CrediballElement = class _CrediballElement extends HTMLElement {
4
8
  constructor() {
5
9
  super(...arguments);
@@ -42,9 +46,29 @@ var _CrediballElement = class _CrediballElement extends HTMLElement {
42
46
  const lowCreditThreshold = thresholdAttr != null ? Number(thresholdAttr) : void 0;
43
47
  this.client = getCrediballClient({ publishableKey, userId, apiUrl, lowCreditThreshold });
44
48
  this.client.start();
45
- this.unsubscribe = this.client.subscribe(() => this.render());
49
+ this.unsubscribe = this.client.subscribe(() => {
50
+ this.applyRemoteTheme();
51
+ this.render();
52
+ });
53
+ this.applyRemoteTheme();
46
54
  this.render();
47
55
  }
56
+ /**
57
+ * Apply the developer-published theme (served alongside packages) as
58
+ * `--crediball-*` custom properties on this element — custom properties pierce
59
+ * the shadow boundary, so the shadow-DOM styles pick them up. On by default;
60
+ * set `apply-remote-theme="false"` to theme from your own CSS instead. The
61
+ * published theme takes precedence over host `--crediball-*` vars (dashboard wins).
62
+ */
63
+ applyRemoteTheme() {
64
+ if (this.getAttribute("apply-remote-theme") === "false") return;
65
+ const theme = this.getSnapshot()?.packages?.ui?.theme;
66
+ if (!theme) return;
67
+ for (const [cssVar, value] of Object.entries(themeToCssVars(theme))) {
68
+ this.style.setProperty(cssVar, value);
69
+ }
70
+ if (theme.fontUrl) loadRemoteFont(theme.fontUrl);
71
+ }
48
72
  teardownClient() {
49
73
  this.client?.stop();
50
74
  this.unsubscribe?.();
@@ -64,9 +88,7 @@ _CrediballElement.CLIENT_ATTRS = /* @__PURE__ */ new Set([
64
88
  var CrediballElement = _CrediballElement;
65
89
 
66
90
  // src/format.ts
67
- function formatCredits(credits) {
68
- return new Intl.NumberFormat(void 0).format(Math.round(credits));
69
- }
91
+ import { formatCredits } from "@crediball/core";
70
92
 
71
93
  // src/balance.ts
72
94
  var CrediballBalanceElement = class extends CrediballElement {
@@ -126,6 +148,8 @@ var CrediballTopUpButtonElement = class extends CrediballElement {
126
148
  this.button.addEventListener("click", () => this.handleClick());
127
149
  }
128
150
  render() {
151
+ const slot = this.button.querySelector("slot");
152
+ if (slot) slot.textContent = this.getSnapshot()?.packages?.ui?.content?.topUpButtonText ?? "Add credits";
129
153
  }
130
154
  handleClick() {
131
155
  const paywall = document.querySelector("crediball-paywall");
@@ -138,6 +162,7 @@ var CrediballTopUpButtonElement = class extends CrediballElement {
138
162
  };
139
163
 
140
164
  // src/low-credit-warning.ts
165
+ import { isBalanceLow } from "@crediball/core";
141
166
  var CrediballLowCreditWarningElement = class extends CrediballElement {
142
167
  static get observedAttributes() {
143
168
  return [...CrediballElement.observedAttributes, "threshold"];
@@ -149,21 +174,18 @@ var CrediballLowCreditWarningElement = class extends CrediballElement {
149
174
  }
150
175
  render() {
151
176
  const snapshot = this.getSnapshot();
177
+ const content = snapshot?.packages?.ui?.content;
152
178
  const thresholdAttr = this.getAttribute("threshold");
153
- const threshold = thresholdAttr != null ? Number(thresholdAttr) : void 0;
154
- const isLow = threshold != null && snapshot?.balance != null ? snapshot.balance < threshold : !!snapshot?.isLow;
179
+ const threshold = thresholdAttr != null ? Number(thresholdAttr) : content?.lowCreditThreshold;
180
+ const isLow = isBalanceLow(snapshot?.balance ?? null, threshold, !!snapshot?.isLow);
155
181
  this.style.display = isLow ? "" : "none";
182
+ const slot = this.shadowRoot?.querySelector("slot");
183
+ if (slot) slot.textContent = content?.lowCreditMessage ?? "Low credit balance \u2014 top up to keep going.";
156
184
  }
157
185
  };
158
186
 
159
187
  // src/paywall.ts
160
- function formatMoney(amount, currency) {
161
- try {
162
- return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
163
- } catch {
164
- return `${amount.toFixed(2)} ${currency}`;
165
- }
166
- }
188
+ import { formatMoney } from "@crediball/core";
167
189
  var CURRENCY_SYMBOLS = { EUR: "\u20AC", USD: "$", GBP: "\xA3" };
168
190
  function currencySymbolFor(currency) {
169
191
  return CURRENCY_SYMBOLS[currency.toUpperCase()] ?? currency;
@@ -285,9 +307,11 @@ var CrediballPaywallElement = class extends CrediballElement {
285
307
  }
286
308
  const snapshot = this.getSnapshot();
287
309
  const pkgs = snapshot?.packages;
310
+ const uiContent = pkgs?.ui?.content;
288
311
  const currency = pkgs?.currency ?? "EUR";
289
- const title = this.getAttribute("title") ?? "You need more credits to continue.";
290
- const description = this.getAttribute("description") ?? "Top up to keep using this feature.";
312
+ const title = this.getAttribute("title") ?? uiContent?.paywallTitle ?? "You need more credits to continue.";
313
+ const description = this.getAttribute("description") ?? uiContent?.paywallDescription ?? "Top up to keep using this feature.";
314
+ const addButtonText = uiContent?.paywallButtonText ?? "Add";
291
315
  const amountsAttr = this.getAttribute("amounts");
292
316
  const legacyAmounts = amountsAttr ? amountsAttr.split(",").map((s) => Number(s.trim())).filter((n) => Number.isFinite(n)) : [5, 10];
293
317
  const packages = pkgs?.packages ?? [];
@@ -330,7 +354,7 @@ var CrediballPaywallElement = class extends CrediballElement {
330
354
  ${showCustom ? `<div class="custom-row">
331
355
  <span class="sym">${escapeHtml(currencySymbolFor(currency))}</span>
332
356
  <input class="custom-input" inputmode="decimal" placeholder="${custom?.minEur ?? ""}" aria-label="Custom amount" />
333
- <button class="custom-add" type="button">Add</button>
357
+ <button class="custom-add" type="button">${escapeHtml(addButtonText)}</button>
334
358
  </div>` : ""}
335
359
  <p class="error" hidden></p>
336
360
  <button class="close" part="close" type="button">Not now</button>
@@ -68,8 +68,21 @@ var Crediball = (() => {
68
68
  return () => window.removeEventListener(BALANCE_CHANGED_EVENT, listener);
69
69
  }
70
70
 
71
+ // ../core/dist/http.js
72
+ var DEFAULT_API_URL = "https://www.crediball.ai/api";
73
+ async function errorMessage(res, fallback) {
74
+ const body = await res.json().catch(() => null);
75
+ return body && typeof body === "object" && "error" in body && String(body.error) || fallback;
76
+ }
77
+ async function fetchJson(url, init, failureLabel = `Request to ${url}`) {
78
+ const res = await fetch(url, init);
79
+ if (!res.ok) {
80
+ throw new Error(await errorMessage(res, `${failureLabel} failed with ${res.status}`));
81
+ }
82
+ return res.json();
83
+ }
84
+
71
85
  // ../core/dist/client.js
72
- var DEFAULT_API_URL = "https://crediball.app/api";
73
86
  var DEFAULT_POLL_MS = 3e4;
74
87
  var SSE_HEALTHY_POLL_MULTIPLIER = 2;
75
88
  var MAX_SSE_FAILURES = 2;
@@ -85,15 +98,6 @@ var Crediball = (() => {
85
98
  error: null
86
99
  };
87
100
  }
88
- async function fetchJson(url, headers) {
89
- const res = await fetch(url, { headers });
90
- if (!res.ok) {
91
- const body = await res.json().catch(() => null);
92
- const message = body && typeof body === "object" && "error" in body && String(body.error) || `Request to ${url} failed with ${res.status}`;
93
- throw new Error(message);
94
- }
95
- return res.json();
96
- }
97
101
  function computeIsLow(balance, costs, explicitThreshold) {
98
102
  if (balance == null)
99
103
  return false;
@@ -134,10 +138,10 @@ var Crediball = (() => {
134
138
  const qs = `userId=${encodeURIComponent(userId)}`;
135
139
  try {
136
140
  const [balanceRes, usageRes, costsRes, packagesRes] = await Promise.all([
137
- fetchJson(`${this.apiUrl}/public/balance?${qs}`, headers),
138
- fetchJson(`${this.apiUrl}/public/usage?${qs}`, headers),
139
- fetchJson(`${this.apiUrl}/public/costs`, headers),
140
- fetchJson(`${this.apiUrl}/public/packages?${qs}`, headers)
141
+ fetchJson(`${this.apiUrl}/public/balance?${qs}`, { headers }),
142
+ fetchJson(`${this.apiUrl}/public/usage?${qs}`, { headers }),
143
+ fetchJson(`${this.apiUrl}/public/costs`, { headers }),
144
+ fetchJson(`${this.apiUrl}/public/packages?${qs}`, { headers })
141
145
  ]);
142
146
  const costs = {};
143
147
  for (const e of costsRes.events)
@@ -188,22 +192,16 @@ var Crediball = (() => {
188
192
  this.apiUrl = (config.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
189
193
  this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_MS;
190
194
  }
191
- async postCheckout(path, input) {
195
+ postCheckout(path, input) {
192
196
  const { publishableKey, userId } = this.config;
193
- const res = await fetch(`${this.apiUrl}${path}`, {
197
+ return fetchJson(`${this.apiUrl}${path}`, {
194
198
  method: "POST",
195
199
  headers: {
196
200
  "Content-Type": "application/json",
197
201
  Authorization: `Bearer ${publishableKey}`
198
202
  },
199
203
  body: JSON.stringify({ userId, ...input })
200
- });
201
- if (!res.ok) {
202
- const body = await res.json().catch(() => null);
203
- const message = body && typeof body === "object" && "error" in body && String(body.error) || `Checkout request failed with ${res.status}`;
204
- throw new Error(message);
205
- }
206
- return res.json();
204
+ }, "Checkout request");
207
205
  }
208
206
  pause() {
209
207
  this.stopPolling();
@@ -303,6 +301,60 @@ var Crediball = (() => {
303
301
  return client;
304
302
  }
305
303
 
304
+ // ../core/dist/format.js
305
+ function formatCredits(credits) {
306
+ return new Intl.NumberFormat(void 0).format(Math.round(credits));
307
+ }
308
+ function formatMoney(amount, currency) {
309
+ try {
310
+ return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
311
+ } catch {
312
+ return `${amount.toFixed(2)} ${currency}`;
313
+ }
314
+ }
315
+ function isBalanceLow(balance, threshold, fallback) {
316
+ return threshold != null && balance != null ? balance < threshold : fallback;
317
+ }
318
+
319
+ // ../core/dist/theme.js
320
+ var THEME_CSS_VAR_ENTRIES = [
321
+ ["accent", "--crediball-accent", String],
322
+ ["onAccent", "--crediball-on-accent", String],
323
+ ["ink", "--crediball-ink", String],
324
+ ["canvas", "--crediball-canvas", String],
325
+ ["hairline", "--crediball-hairline", String],
326
+ ["radius", "--crediball-radius", (v) => `${v}px`],
327
+ ["radiusCard", "--crediball-radius-card", (v) => `${v}px`],
328
+ ["fontStack", "--crediball-font", String]
329
+ ];
330
+ function themeToCssVars(theme) {
331
+ const vars = {};
332
+ if (!theme)
333
+ return vars;
334
+ for (const [key, cssVar, fmt] of THEME_CSS_VAR_ENTRIES) {
335
+ const value = theme[key];
336
+ if (value != null && value !== "")
337
+ vars[cssVar] = fmt(value);
338
+ }
339
+ return vars;
340
+ }
341
+ function loadRemoteFont(url) {
342
+ if (typeof document === "undefined")
343
+ return;
344
+ const existing = document.querySelector("link[data-crediball-font]");
345
+ if (existing?.href === url)
346
+ return;
347
+ if (existing)
348
+ existing.href = url;
349
+ else {
350
+ const link = document.createElement("link");
351
+ link.rel = "stylesheet";
352
+ link.href = url;
353
+ link.setAttribute("data-crediball-font", "");
354
+ document.head.appendChild(link);
355
+ }
356
+ }
357
+
306
358
  // src/base.ts
307
359
  var _CrediballElement = class _CrediballElement extends HTMLElement {
308
360
  constructor() {
@@ -346,9 +398,29 @@ var Crediball = (() => {
346
398
  const lowCreditThreshold = thresholdAttr != null ? Number(thresholdAttr) : void 0;
347
399
  this.client = getCrediballClient({ publishableKey, userId, apiUrl, lowCreditThreshold });
348
400
  this.client.start();
349
- this.unsubscribe = this.client.subscribe(() => this.render());
401
+ this.unsubscribe = this.client.subscribe(() => {
402
+ this.applyRemoteTheme();
403
+ this.render();
404
+ });
405
+ this.applyRemoteTheme();
350
406
  this.render();
351
407
  }
408
+ /**
409
+ * Apply the developer-published theme (served alongside packages) as
410
+ * `--crediball-*` custom properties on this element — custom properties pierce
411
+ * the shadow boundary, so the shadow-DOM styles pick them up. On by default;
412
+ * set `apply-remote-theme="false"` to theme from your own CSS instead. The
413
+ * published theme takes precedence over host `--crediball-*` vars (dashboard wins).
414
+ */
415
+ applyRemoteTheme() {
416
+ if (this.getAttribute("apply-remote-theme") === "false") return;
417
+ const theme = this.getSnapshot()?.packages?.ui?.theme;
418
+ if (!theme) return;
419
+ for (const [cssVar, value] of Object.entries(themeToCssVars(theme))) {
420
+ this.style.setProperty(cssVar, value);
421
+ }
422
+ if (theme.fontUrl) loadRemoteFont(theme.fontUrl);
423
+ }
352
424
  teardownClient() {
353
425
  this.client?.stop();
354
426
  this.unsubscribe?.();
@@ -367,11 +439,6 @@ var Crediball = (() => {
367
439
  ]);
368
440
  var CrediballElement = _CrediballElement;
369
441
 
370
- // src/format.ts
371
- function formatCredits(credits) {
372
- return new Intl.NumberFormat(void 0).format(Math.round(credits));
373
- }
374
-
375
442
  // src/balance.ts
376
443
  var CrediballBalanceElement = class extends CrediballElement {
377
444
  constructor() {
@@ -430,6 +497,8 @@ var Crediball = (() => {
430
497
  this.button.addEventListener("click", () => this.handleClick());
431
498
  }
432
499
  render() {
500
+ const slot = this.button.querySelector("slot");
501
+ if (slot) slot.textContent = this.getSnapshot()?.packages?.ui?.content?.topUpButtonText ?? "Add credits";
433
502
  }
434
503
  handleClick() {
435
504
  const paywall = document.querySelector("crediball-paywall");
@@ -453,21 +522,17 @@ var Crediball = (() => {
453
522
  }
454
523
  render() {
455
524
  const snapshot = this.getSnapshot();
525
+ const content = snapshot?.packages?.ui?.content;
456
526
  const thresholdAttr = this.getAttribute("threshold");
457
- const threshold = thresholdAttr != null ? Number(thresholdAttr) : void 0;
458
- const isLow = threshold != null && snapshot?.balance != null ? snapshot.balance < threshold : !!snapshot?.isLow;
527
+ const threshold = thresholdAttr != null ? Number(thresholdAttr) : content?.lowCreditThreshold;
528
+ const isLow = isBalanceLow(snapshot?.balance ?? null, threshold, !!snapshot?.isLow);
459
529
  this.style.display = isLow ? "" : "none";
530
+ const slot = this.shadowRoot?.querySelector("slot");
531
+ if (slot) slot.textContent = content?.lowCreditMessage ?? "Low credit balance \u2014 top up to keep going.";
460
532
  }
461
533
  };
462
534
 
463
535
  // src/paywall.ts
464
- function formatMoney(amount, currency) {
465
- try {
466
- return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
467
- } catch {
468
- return `${amount.toFixed(2)} ${currency}`;
469
- }
470
- }
471
536
  var CURRENCY_SYMBOLS = { EUR: "\u20AC", USD: "$", GBP: "\xA3" };
472
537
  function currencySymbolFor(currency) {
473
538
  return CURRENCY_SYMBOLS[currency.toUpperCase()] ?? currency;
@@ -589,9 +654,11 @@ var Crediball = (() => {
589
654
  }
590
655
  const snapshot = this.getSnapshot();
591
656
  const pkgs = snapshot?.packages;
657
+ const uiContent = pkgs?.ui?.content;
592
658
  const currency = pkgs?.currency ?? "EUR";
593
- const title = this.getAttribute("title") ?? "You need more credits to continue.";
594
- const description = this.getAttribute("description") ?? "Top up to keep using this feature.";
659
+ const title = this.getAttribute("title") ?? uiContent?.paywallTitle ?? "You need more credits to continue.";
660
+ const description = this.getAttribute("description") ?? uiContent?.paywallDescription ?? "Top up to keep using this feature.";
661
+ const addButtonText = uiContent?.paywallButtonText ?? "Add";
595
662
  const amountsAttr = this.getAttribute("amounts");
596
663
  const legacyAmounts = amountsAttr ? amountsAttr.split(",").map((s) => Number(s.trim())).filter((n) => Number.isFinite(n)) : [5, 10];
597
664
  const packages = pkgs?.packages ?? [];
@@ -634,7 +701,7 @@ var Crediball = (() => {
634
701
  ${showCustom ? `<div class="custom-row">
635
702
  <span class="sym">${escapeHtml(currencySymbolFor(currency))}</span>
636
703
  <input class="custom-input" inputmode="decimal" placeholder="${custom?.minEur ?? ""}" aria-label="Custom amount" />
637
- <button class="custom-add" type="button">Add</button>
704
+ <button class="custom-add" type="button">${escapeHtml(addButtonText)}</button>
638
705
  </div>` : ""}
639
706
  <p class="error" hidden></p>
640
707
  <button class="close" part="close" type="button">Not now</button>
package/dist/index.d.ts CHANGED
@@ -24,6 +24,14 @@ declare abstract class CrediballElement extends HTMLElement {
24
24
  */
25
25
  attributeChangedCallback(name: string): void;
26
26
  private connectClient;
27
+ /**
28
+ * Apply the developer-published theme (served alongside packages) as
29
+ * `--crediball-*` custom properties on this element — custom properties pierce
30
+ * the shadow boundary, so the shadow-DOM styles pick them up. On by default;
31
+ * set `apply-remote-theme="false"` to theme from your own CSS instead. The
32
+ * published theme takes precedence over host `--crediball-*` vars (dashboard wins).
33
+ */
34
+ private applyRemoteTheme;
27
35
  private teardownClient;
28
36
  protected getSnapshot(): CrediballSnapshot | null;
29
37
  /** Called on connect, on relevant attribute changes, and on every data update. */
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  CrediballPaywallElement,
6
6
  CrediballTopUpButtonElement,
7
7
  CrediballUsageElement
8
- } from "./chunk-AEVAHMRY.js";
8
+ } from "./chunk-WUPVPARI.js";
9
9
  export {
10
10
  CrediballBalanceElement,
11
11
  CrediballElement,
package/dist/register.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  CrediballPaywallElement,
5
5
  CrediballTopUpButtonElement,
6
6
  CrediballUsageElement
7
- } from "./chunk-AEVAHMRY.js";
7
+ } from "./chunk-WUPVPARI.js";
8
8
 
9
9
  // src/register.ts
10
10
  function define(name, ctor) {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@crediball/elements",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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>",
7
- "homepage": "https://crediball.app",
7
+ "homepage": "https://crediball.ai",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/filipporezzadore/Crediball.git",
@@ -56,7 +56,7 @@
56
56
  "prepublishOnly": "npm run build"
57
57
  },
58
58
  "dependencies": {
59
- "@crediball/core": "^0.2.0"
59
+ "@crediball/core": "^0.4.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "tsup": "^8.3.5",