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