@contentcredits/sdk 2.15.0 → 2.16.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.
@@ -647,183 +647,229 @@ function getPaywallStyles(primaryColor, fontFamily) {
647
647
  return `
648
648
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
649
649
 
650
- /* ── Inline paywall panel sits below teaser in the page flow ── */
650
+ /* ─── Inline paywall panel ──────────────────────────────────────────── */
651
651
  .cc-paywall-inline {
652
652
  width: 100%;
653
- padding: 36px 24px 32px;
653
+ padding: 32px 28px 28px;
654
654
  background: #fff;
655
- border: 1px solid #e5e7eb;
655
+ border: 1px solid #e2e8f0;
656
656
  border-top: 3px solid ${primaryColor};
657
657
  border-radius: 0 0 12px 12px;
658
658
  text-align: center;
659
659
  font-family: ${fontFamily};
660
- box-sizing: border-box;
661
660
  }
662
-
663
661
  .cc-paywall-inline h2 {
664
662
  font-size: 20px;
665
663
  font-weight: 700;
666
- color: #111827;
664
+ color: #0f172a;
667
665
  margin-bottom: 8px;
666
+ letter-spacing: -0.015em;
667
+ line-height: 1.25;
668
668
  }
669
-
670
669
  .cc-paywall-inline p {
671
670
  font-size: 14px;
672
- color: #6b7280;
673
- margin-bottom: 24px;
671
+ color: #64748b;
674
672
  line-height: 1.6;
673
+ margin-bottom: 20px;
675
674
  }
676
675
 
677
- /* ── Overlay paywall panel full-width white panel below gated content ── */
676
+ /* ─── Overlay paywall panel ─────────────────────────────────────────── */
677
+ /*
678
+ * Fixed to the bottom of the viewport, full width.
679
+ * Elevation uses layered shadows — no harsh border-top line.
680
+ * Thin hairline at the top (1px shadow) + ambient shadow for depth.
681
+ */
678
682
  .cc-paywall-overlay {
679
- /* Fixed to the bottom of the viewport — always visible, full width */
680
683
  position: fixed;
681
684
  bottom: 0;
682
685
  left: 0;
683
686
  width: 100%;
684
687
  background: #fff;
685
- box-shadow: 0 -8px 40px rgba(0,0,0,0.10);
686
- border-top: 1px solid #e5e7eb;
688
+ box-shadow:
689
+ 0 -1px 0 rgba(15, 23, 42, 0.07),
690
+ 0 -4px 12px rgba(15, 23, 42, 0.04),
691
+ 0 -24px 64px rgba(15, 23, 42, 0.07);
687
692
  font-family: ${fontFamily};
688
693
  }
689
694
 
690
- /* Gradient that fades the article into the panel.
691
- Sits above the panel via position:absolute + negative top. */
695
+ /*
696
+ * Gradient that fades the article content into the panel.
697
+ * Multi-stop with a cubic-ease-ish curve so it reads as depth,
698
+ * not as an obvious white overlay.
699
+ * initOverlay() overrides this via inline style with the real page bg colour.
700
+ */
692
701
  .cc-paywall-overlay-gradient {
693
702
  position: absolute;
694
- top: -120px;
703
+ top: -160px;
695
704
  left: 0;
696
705
  right: 0;
697
- height: 120px;
698
- background: linear-gradient(to bottom, transparent 0%, #fff 100%);
706
+ height: 160px;
699
707
  pointer-events: none;
708
+ background: linear-gradient(
709
+ to bottom,
710
+ transparent 0%,
711
+ rgba(255,255,255,0.08) 30%,
712
+ rgba(255,255,255,0.55) 60%,
713
+ rgba(255,255,255,0.90) 82%,
714
+ #ffffff 100%
715
+ );
700
716
  }
701
717
 
702
- /* Top slot — client-supplied content */
718
+ /* Top slot — publisher-supplied content */
703
719
  .cc-paywall-overlay-slot {
704
- padding: 20px 24px 0;
720
+ padding: 18px 24px 0;
705
721
  display: flex;
706
722
  flex-direction: column;
707
723
  align-items: center;
708
- gap: 10px;
724
+ gap: 5px;
709
725
  }
710
726
 
711
- /* Our SDK's unlock section below the slot */
727
+ /* SDK's own action section below the slot */
712
728
  .cc-paywall-overlay-body {
713
- padding: 16px 24px 24px;
729
+ padding: 14px 24px 26px;
714
730
  display: flex;
715
731
  flex-direction: column;
716
732
  align-items: center;
717
- gap: 0;
733
+ gap: 12px;
718
734
  text-align: center;
719
735
  }
720
736
 
721
- /* Slot item heading */
737
+ /* ─── Slot typography ───────────────────────────────────────────────── */
722
738
  .cc-slot-heading {
723
- font-size: 22px;
739
+ font-size: 19px;
724
740
  font-weight: 700;
725
- color: #111827;
741
+ color: #0f172a;
726
742
  text-align: center;
727
743
  line-height: 1.3;
744
+ letter-spacing: -0.015em;
728
745
  }
729
-
730
- /* Slot item — subheading */
731
746
  .cc-slot-subheading {
732
- font-size: 16px;
747
+ font-size: 15px;
733
748
  font-weight: 600;
734
- color: #374151;
749
+ color: #1e293b;
735
750
  text-align: center;
736
751
  }
737
-
738
- /* Slot item — body text */
739
752
  .cc-slot-text {
740
- font-size: 14px;
741
- color: #6b7280;
753
+ font-size: 13px;
754
+ color: #64748b;
742
755
  text-align: center;
743
- line-height: 1.6;
756
+ line-height: 1.55;
744
757
  }
745
758
 
746
- /* Slot item divider with optional label */
759
+ /* Visual separator between slot and body */
747
760
  .cc-slot-divider {
748
761
  display: flex;
749
762
  align-items: center;
750
- gap: 12px;
751
- width: 100%;
752
- max-width: 320px;
753
- font-size: 13px;
754
- color: #9ca3af;
763
+ gap: 10px;
764
+ font-size: 12px;
765
+ color: #94a3b8;
755
766
  }
756
767
  .cc-slot-divider::before,
757
768
  .cc-slot-divider::after {
758
769
  content: '';
759
770
  flex: 1;
760
- border-top: 1px solid #e5e7eb;
771
+ height: 1px;
772
+ background: #e2e8f0;
761
773
  }
762
774
 
775
+ /* ─── Buttons ───────────────────────────────────────────────────────── */
776
+ /*
777
+ * All primary paywall CTAs use .cc-btn-primary (filled, brand colour).
778
+ * .cc-btn-ghost is for low-emphasis secondary links.
779
+ * No outline variant in paywall states — one clear hierarchy.
780
+ */
763
781
  .cc-btn {
764
782
  display: inline-flex;
765
783
  align-items: center;
766
784
  justify-content: center;
767
- gap: 8px;
768
- height: 44px;
769
- padding: 0 20px;
785
+ gap: 7px;
786
+ height: 46px;
787
+ padding: 0 22px;
770
788
  border: none;
771
- border-radius: 8px;
789
+ border-radius: 10px;
772
790
  font-family: ${fontFamily};
773
- font-size: 15px;
791
+ font-size: 14px;
774
792
  font-weight: 600;
775
793
  cursor: pointer;
776
- transition: opacity 0.15s ease, transform 0.1s ease;
794
+ transition: filter 0.15s ease, transform 0.1s ease;
777
795
  width: 100%;
778
- max-width: 320px;
796
+ max-width: 380px;
797
+ letter-spacing: -0.01em;
798
+ white-space: nowrap;
799
+ flex-shrink: 0;
779
800
  }
780
- .cc-btn:hover:not(:disabled) { opacity: 0.88; }
781
- .cc-btn:active:not(:disabled) { transform: scale(0.98); }
801
+ .cc-btn:hover:not(:disabled) { filter: brightness(1.07); }
802
+ .cc-btn:active:not(:disabled) { transform: scale(0.975); }
782
803
  .cc-btn:disabled { opacity: 0.55; cursor: not-allowed; }
783
804
 
784
- .cc-btn-primary {
785
- background: ${primaryColor};
786
- color: #fff;
787
- }
805
+ .cc-btn-primary { background: ${primaryColor}; color: #fff; }
806
+ .cc-btn-secondary { background: #0f172a; color: #fff; }
788
807
 
789
- .cc-btn-secondary {
790
- background: #111827;
791
- color: #fff;
792
- margin-top: 10px;
808
+ /* Ghost: text-link-style secondary action */
809
+ .cc-btn-ghost {
810
+ background: transparent;
811
+ color: #64748b;
812
+ height: 36px;
813
+ font-size: 13px;
814
+ font-weight: 500;
815
+ letter-spacing: 0;
816
+ }
817
+ .cc-btn-ghost:hover:not(:disabled) {
818
+ color: #0f172a;
819
+ background: #f1f5f9;
820
+ filter: none;
793
821
  }
794
822
 
823
+ /* Kept for backwards-compat with paywallTopSlot button items */
795
824
  .cc-btn-outline {
796
825
  background: transparent;
797
- color: #111827;
798
- border: 2px solid #111827;
799
- margin-top: 10px;
826
+ color: #0f172a;
827
+ border: 1.5px solid #cbd5e1;
828
+ }
829
+ .cc-btn-outline:hover:not(:disabled) {
830
+ border-color: #94a3b8;
831
+ filter: none;
832
+ background: #f8fafc;
800
833
  }
801
834
 
835
+ /* ─── Credit badge ───────────────────────────────────────────────────── */
802
836
  .cc-credit-badge {
803
- display: inline-block;
804
- background: #fef3c7;
805
- color: #92400e;
837
+ display: inline-flex;
838
+ align-items: center;
839
+ background: #f1f5f9;
840
+ color: #475569;
806
841
  border-radius: 20px;
807
- padding: 2px 10px;
808
- font-size: 13px;
809
- font-weight: 600;
810
- margin-bottom: 16px;
842
+ padding: 3px 10px;
843
+ font-size: 12px;
844
+ font-weight: 700;
845
+ letter-spacing: 0.01em;
846
+ }
847
+
848
+ /* ─── Inline state description text ─────────────────────────────────── */
849
+ .cc-state-detail {
850
+ font-size: 14px;
851
+ color: #64748b;
852
+ line-height: 1.6;
811
853
  }
812
854
 
855
+ /* ─── Spinner (lives inside .cc-btn-primary while loading) ──────────── */
813
856
  .cc-spinner {
814
- width: 18px; height: 18px;
815
- border: 2px solid rgba(255,255,255,0.4);
816
- border-top-color: #fff;
857
+ display: inline-block;
858
+ width: 17px;
859
+ height: 17px;
860
+ border: 2px solid rgba(255, 255, 255, 0.3);
861
+ border-top-color: rgba(255, 255, 255, 0.95);
817
862
  border-radius: 50%;
818
- animation: cc-spin 0.7s linear infinite;
863
+ animation: cc-spin 0.6s linear infinite;
819
864
  flex-shrink: 0;
820
865
  }
821
866
  @keyframes cc-spin { to { transform: rotate(360deg); } }
822
867
 
868
+ /* ─── "Powered by" attribution ───────────────────────────────────────── */
823
869
  .cc-powered-by {
824
- margin-top: 20px;
825
- font-size: 12px;
826
- color: #9ca3af;
870
+ font-size: 11px;
871
+ color: #94a3b8;
872
+ letter-spacing: 0.01em;
827
873
  }
828
874
  .cc-powered-by a {
829
875
  color: ${primaryColor};
@@ -831,6 +877,13 @@ function getPaywallStyles(primaryColor, fontFamily) {
831
877
  font-weight: 600;
832
878
  }
833
879
  .cc-powered-by a:hover { text-decoration: underline; }
880
+
881
+ /* ─── Mobile: tighter vertical padding ──────────────────────────────── */
882
+ @media (max-width: 480px) {
883
+ .cc-paywall-overlay-slot { padding: 14px 16px 0; gap: 4px; }
884
+ .cc-paywall-overlay-body { padding: 12px 16px 20px; gap: 10px; }
885
+ .cc-slot-heading { font-size: 17px; }
886
+ }
834
887
  `;
835
888
  }
836
889
  function getCommentStyles(primaryColor, fontFamily) {
@@ -1322,7 +1375,23 @@ function createPaywallRenderer(config) {
1322
1375
  gradient.className = 'cc-paywall-overlay-gradient';
1323
1376
  const pageBg = getComputedStyle(document.body).backgroundColor;
1324
1377
  if (pageBg && pageBg !== 'rgba(0, 0, 0, 0)' && pageBg !== 'transparent') {
1325
- gradient.style.background = `linear-gradient(to bottom, transparent 0%, ${pageBg} 100%)`;
1378
+ // Multi-stop gradient so the fade reads as natural depth rather than a
1379
+ // sharp white overlay — same curve as the CSS default, but using the
1380
+ // actual page background colour.
1381
+ gradient.style.background = [
1382
+ 'linear-gradient(to bottom,',
1383
+ `transparent 0%,`,
1384
+ `transparent 18%,`,
1385
+ // Mid-stops approximate a cubic ease-in curve
1386
+ `color-mix(in srgb, ${pageBg} 30%, transparent) 45%,`,
1387
+ `color-mix(in srgb, ${pageBg} 75%, transparent) 68%,`,
1388
+ `${pageBg} 100%`,
1389
+ ')',
1390
+ ].join(' ');
1391
+ // Fallback for browsers without color-mix (pre-2023) — simple 2-stop
1392
+ if (!CSS.supports('color', 'color-mix(in srgb, red 50%, blue)')) {
1393
+ gradient.style.background = `linear-gradient(to bottom, transparent 0%, ${pageBg} 100%)`;
1394
+ }
1326
1395
  }
1327
1396
  panel.appendChild(gradient);
1328
1397
  // Add bottom padding to the content element so the fixed panel
@@ -1333,12 +1402,13 @@ function createPaywallRenderer(config) {
1333
1402
  slot.className = 'cc-paywall-overlay-slot';
1334
1403
  reactRoot = (_a = mountTopSlot(slot, config.paywallTopSlot, config.reactDOM)) !== null && _a !== void 0 ? _a : null;
1335
1404
  panel.appendChild(slot);
1336
- // Only render the "or" divider between slot and body when the slot has content
1405
+ // Visual separator between slot and body — only when the slot has content.
1406
+ // Rendered as a plain horizontal line (no "or" text) so it works regardless
1407
+ // of whether the slot contains competing CTAs or just descriptive copy.
1337
1408
  if (config.paywallTopSlot) {
1338
1409
  const divider = el('div');
1339
1410
  divider.className = 'cc-slot-divider';
1340
- divider.style.cssText = 'margin: 4px auto 0; padding: 0 24px;';
1341
- divider.textContent = 'or';
1411
+ divider.style.cssText = 'margin: 6px 24px 0;';
1342
1412
  panel.appendChild(divider);
1343
1413
  }
1344
1414
  // Our SDK's unlock body
@@ -1355,6 +1425,18 @@ function createPaywallRenderer(config) {
1355
1425
  init();
1356
1426
  if (!body)
1357
1427
  return;
1428
+ // Loading: don't rebuild the DOM — freeze the active button in place.
1429
+ // This prevents the panel from shrinking/shifting when a purchase or
1430
+ // login is in progress, which would be jarring given the panel's fixed position.
1431
+ if (state === 'loading') {
1432
+ setButtonLoading(true);
1433
+ return;
1434
+ }
1435
+ if (state === 'granted') {
1436
+ destroy();
1437
+ return;
1438
+ }
1439
+ // Full state transition — clear and rebuild the body.
1358
1440
  while (body.firstChild)
1359
1441
  body.removeChild(body.firstChild);
1360
1442
  switch (state) {
@@ -1367,22 +1449,19 @@ function createPaywallRenderer(config) {
1367
1449
  case 'insufficient':
1368
1450
  renderInsufficient(body, callbacks, (_b = meta === null || meta === void 0 ? void 0 : meta.requiredCredits) !== null && _b !== void 0 ? _b : null, (_c = meta === null || meta === void 0 ? void 0 : meta.creditBalance) !== null && _c !== void 0 ? _c : null);
1369
1451
  break;
1370
- case 'loading':
1371
- renderLoading(body);
1372
- break;
1373
- case 'granted':
1374
- destroy();
1375
- break;
1376
1452
  }
1377
1453
  }
1454
+ // ── State renderers ────────────────────────────────────────────────────────
1378
1455
  function renderLogin(parent, cb) {
1379
- // Only show the heading/description in inline mode; in overlay mode the
1380
- // client's top slot already provides the article context.
1456
+ // In overlay mode the slot already provides article context, so the body
1457
+ // just needs the CTA. In inline mode we add heading + description.
1381
1458
  if (config.paywallMode === 'inline') {
1382
1459
  parent.appendChild(el('h2', 'This article requires a subscription'));
1383
- parent.appendChild(el('p', 'Log in with your Content Credits account to unlock this article.'));
1460
+ const detail = el('p', 'Sign in to your Content Credits account to unlock this article.');
1461
+ detail.className = 'cc-state-detail';
1462
+ parent.appendChild(detail);
1384
1463
  }
1385
- const btn = el('button', 'Login & Unlock with Content Credits');
1464
+ const btn = el('button', 'Sign in to read');
1386
1465
  btn.className = 'cc-btn cc-btn-primary';
1387
1466
  btn.addEventListener('click', () => { void cb.onLogin(); });
1388
1467
  parent.appendChild(btn);
@@ -1391,18 +1470,16 @@ function createPaywallRenderer(config) {
1391
1470
  function renderPurchase(parent, cb, credits) {
1392
1471
  if (config.paywallMode === 'inline') {
1393
1472
  parent.appendChild(el('h2', 'Unlock this article'));
1394
- if (credits !== null) {
1395
- const badge = el('span', `${credits} credit${credits !== 1 ? 's' : ''}`);
1396
- badge.className = 'cc-credit-badge';
1397
- parent.appendChild(badge);
1398
- }
1399
- parent.appendChild(el('p', 'Use your Content Credits balance to instantly access this premium article.'));
1473
+ const detail = el('p', 'Use your Content Credits balance to instantly access this article.');
1474
+ detail.className = 'cc-state-detail';
1475
+ parent.appendChild(detail);
1400
1476
  }
1477
+ // Credits shown inline in the button label — clear and scannable.
1401
1478
  const label = credits !== null
1402
- ? `Unlock Just This Story · ${credits} Credit${credits !== 1 ? 's' : ''}`
1403
- : 'Unlock Just This Story';
1479
+ ? `Unlock · ${credits} credit${credits !== 1 ? 's' : ''}`
1480
+ : 'Unlock article';
1404
1481
  const btn = el('button', label);
1405
- btn.className = 'cc-btn cc-btn-outline';
1482
+ btn.className = 'cc-btn cc-btn-primary';
1406
1483
  btn.addEventListener('click', () => { void cb.onPurchase(); });
1407
1484
  parent.appendChild(btn);
1408
1485
  parent.appendChild(poweredBy());
@@ -1411,26 +1488,21 @@ function createPaywallRenderer(config) {
1411
1488
  if (config.paywallMode === 'inline') {
1412
1489
  parent.appendChild(el('h2', 'Not enough credits'));
1413
1490
  }
1414
- const detail = required !== null && available !== null
1415
- ? `You need ${required} credit${required !== 1 ? 's' : ''} but only have ${available}.`
1416
- : 'You don\'t have enough credits to unlock this article.';
1417
- parent.appendChild(el('p', detail));
1418
- const btn = el('button', 'Buy More Credits');
1491
+ const detail = el('p');
1492
+ detail.className = 'cc-state-detail';
1493
+ if (required !== null && available !== null) {
1494
+ detail.textContent = `This article costs ${required} credit${required !== 1 ? 's' : ''} — you have ${available}.`;
1495
+ }
1496
+ else {
1497
+ detail.textContent = "You don't have enough credits to unlock this article.";
1498
+ }
1499
+ parent.appendChild(detail);
1500
+ const btn = el('button', 'Top up credits');
1419
1501
  btn.className = 'cc-btn cc-btn-primary';
1420
1502
  btn.addEventListener('click', () => cb.onBuyMoreCredits());
1421
1503
  parent.appendChild(btn);
1422
1504
  parent.appendChild(poweredBy());
1423
1505
  }
1424
- function renderLoading(parent) {
1425
- const btn = el('button');
1426
- btn.className = 'cc-btn cc-btn-outline';
1427
- btn.disabled = true;
1428
- const spinner = el('span');
1429
- spinner.className = 'cc-spinner';
1430
- btn.appendChild(spinner);
1431
- btn.appendChild(document.createTextNode(' Processing…'));
1432
- parent.appendChild(btn);
1433
- }
1434
1506
  function poweredBy() {
1435
1507
  const div = el('div');
1436
1508
  div.className = 'cc-powered-by';
@@ -1442,6 +1514,7 @@ function createPaywallRenderer(config) {
1442
1514
  div.appendChild(link);
1443
1515
  return div;
1444
1516
  }
1517
+ // ── Button loading state ───────────────────────────────────────────────────
1445
1518
  function setButtonLoading(loading) {
1446
1519
  if (!body)
1447
1520
  return;
@@ -1450,6 +1523,8 @@ function createPaywallRenderer(config) {
1450
1523
  return;
1451
1524
  btn.disabled = loading;
1452
1525
  if (loading) {
1526
+ // Replace button content with spinner + label, preserving button size.
1527
+ // The spinner is white on the primary colour background — matches all states.
1453
1528
  const spinner = el('span');
1454
1529
  spinner.className = 'cc-spinner';
1455
1530
  setTextContent(btn, '');
@@ -1457,6 +1532,7 @@ function createPaywallRenderer(config) {
1457
1532
  btn.appendChild(document.createTextNode(' Processing…'));
1458
1533
  }
1459
1534
  }
1535
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
1460
1536
  function destroy() {
1461
1537
  reactRoot === null || reactRoot === void 0 ? void 0 : reactRoot.unmount();
1462
1538
  reactRoot = null;
@@ -3227,7 +3303,7 @@ class ContentCredits {
3227
3303
  }
3228
3304
  /** SDK version string. */
3229
3305
  static get version() {
3230
- return "2.15.0";
3306
+ return "2.16.0";
3231
3307
  }
3232
3308
  }
3233
3309
  // ── Auto-init from script data attributes (CDN usage) ────────────────────────