@ecomconsult/consentkit 0.5.1 → 0.5.3

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.
@@ -28,7 +28,7 @@ export function undecidedState() {
28
28
  */
29
29
  export function createStub() {
30
30
  const stub = {
31
- version: '0.5.1',
31
+ version: '0.5.3',
32
32
  config: {},
33
33
  init: function () { return undecidedState(); },
34
34
  allowed: function (cat) { return cat === 'necessary'; },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomconsult/consentkit",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "GDPR cookie consent core with blocking engine, Shadow DOM UI and Google Consent Mode v2. Zero dependencies, no build step.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/ck-core.js CHANGED
@@ -1461,7 +1461,7 @@
1461
1461
  // Public API
1462
1462
  // ---------------------------------------------------------------------------
1463
1463
  var ConsentKit = {
1464
- version: '0.5.1',
1464
+ version: '0.5.3',
1465
1465
  config: config,
1466
1466
 
1467
1467
  init: function (userConfig) {
package/src/ck-debug.js CHANGED
@@ -203,9 +203,11 @@
203
203
  themeMode: 'тема',
204
204
  themeModeLight: 'светлая',
205
205
  themeModeDark: 'тёмная',
206
- themeFont: 'шрифт',
207
- themeFontInherit: 'как на сайте',
206
+ themeFont: 'Шрифт',
207
+ themeFontInherit: 'наследуется',
208
208
  themeFontSystem: 'системный',
209
+ themeFontPage: 'со страницы',
210
+ themeFontTry: 'попытка',
209
211
  themeRadius: 'скругления',
210
212
  themeCard: 'карточка',
211
213
  themeBtn: 'кнопки',
@@ -262,9 +264,11 @@
262
264
  themeMode: 'theme',
263
265
  themeModeLight: 'light',
264
266
  themeModeDark: 'dark',
265
- themeFont: 'font',
266
- themeFontInherit: 'site font',
267
+ themeFont: 'Font',
268
+ themeFontInherit: 'inherited',
267
269
  themeFontSystem: 'system',
270
+ themeFontPage: 'from the page',
271
+ themeFontTry: 'attempt',
268
272
  themeRadius: 'radii',
269
273
  themeCard: 'card',
270
274
  themeBtn: 'buttons',
@@ -598,6 +602,28 @@
598
602
  return typeof r === 'number' && isFinite(r) ? (Math.round(r * 100) / 100) + ':1' : '?';
599
603
  }
600
604
 
605
+ /* «Шрифт: <значение> (со страницы | системный | наследуется)».
606
+ theme.font:'system' is a fixed stack; otherwise the UI samples real page
607
+ text and only falls back to CSS `inherit` when the page states no family of
608
+ its own — the three cases have to be told apart, because «наследуется» on a
609
+ site whose body is unstyled is exactly the Times bug being diagnosed. */
610
+ function fontRow(built, T) {
611
+ if (built.font !== 'inherit') return built.font + ' (' + T.themeFontSystem + ')';
612
+ var page = null, n = 0;
613
+ try {
614
+ if (CK && typeof CK._resolvePageFont === 'function') page = CK._resolvePageFont();
615
+ } catch (e) { page = null; }
616
+ // The attempt number is the whole point of the 0.5.3 re-probe: a page whose
617
+ // stylesheets land late resolves on look 2 or 3, and «попытка 1» vs
618
+ // «попытка 3» is what tells those two pages apart in the panel.
619
+ try {
620
+ if (CK && typeof CK._pageFontAttempt === 'function') n = CK._pageFontAttempt();
621
+ } catch (e) { n = 0; }
622
+ var tries = (typeof n === 'number' && n > 1) ? ', ' + T.themeFontTry + ' ' + n : '';
623
+ return page ? page + ' (' + T.themeFontPage + tries + ')'
624
+ : 'inherit (' + T.themeFontInherit + tries + ')';
625
+ }
626
+
601
627
  // Resolved lazily, not at parse time: this file runs before ConsentKit.init()
602
628
  // has merged the site's config, so asking for the language now would always
603
629
  // read the built-in default. Re-resolved on every render so a page that
@@ -749,7 +775,7 @@
749
775
  sT.appendChild(defs([
750
776
  [T.themeMode, (dark ? T.themeModeDark : T.themeModeLight) +
751
777
  (built.mode === 'auto' ? ' (auto)' : '')],
752
- [T.themeFont, built.font === 'inherit' ? T.themeFontInherit : T.themeFontSystem],
778
+ [T.themeFont, fontRow(built, T)],
753
779
  [T.themeRadius, T.themeCard + ' ' + built.radius.card + 'px · ' +
754
780
  T.themeBtn + ' ' + built.radius.button + 'px'],
755
781
  [T.themeLink, lkTxt]
package/src/ck-ui.js CHANGED
@@ -663,6 +663,240 @@
663
663
  return (theme && theme.font === 'system') ? SYSTEM_FONT : 'inherit';
664
664
  }
665
665
 
666
+ /* CSS `inherit` on the shadow root takes the family of the HOST <div>, which
667
+ hangs off <body>. On a site that sets its typeface on inner blocks and not
668
+ on body/html, body's computed family IS the browser default (Times), so the
669
+ banner came out in Times on a page that is entirely sans-serif. Reading the
670
+ family off real page TEXT instead is what «как на сайте» was always meant
671
+ to mean.
672
+
673
+ Split in two: pickPageFont is pure arithmetic over already-collected
674
+ samples (unit-testable in node, published on _contrast), resolvePageFont
675
+ does the DOM reading. */
676
+
677
+ // Families are compared as CSS grammar, not as strings: '"Google Sans"' and
678
+ // 'google sans' name the same face, and only the UA default must be rejected.
679
+ function normFamily(v) {
680
+ if (typeof v !== 'string') return '';
681
+ return v.replace(/["']/g, '').replace(/\s*,\s*/g, ',').trim().toLowerCase();
682
+ }
683
+
684
+ /* First sample whose family differs from the UA default wins; null when the
685
+ page never states a family of its own (the caller then keeps `inherit`).
686
+ `samples` is [{ family }] in priority order. */
687
+ function pickPageFont(samples, uaDefault) {
688
+ if (!samples || typeof samples.length !== 'number') return null;
689
+ var ua = normFamily(uaDefault);
690
+ for (var i = 0; i < samples.length; i++) {
691
+ var s = samples[i];
692
+ var fam = s && typeof s.family === 'string' ? s.family.trim() : '';
693
+ if (!fam) continue; // unstyled / detached element
694
+ var norm = normFamily(fam);
695
+ if (!norm) continue;
696
+ if (ua && norm === ua) continue; // still the browser default
697
+ // The value lands in a generated stylesheet. getComputedStyle normalises,
698
+ // but a family that could close the declaration is never worth the risk.
699
+ if (/[;{}]|\/\*/.test(fam)) continue;
700
+ return fam;
701
+ }
702
+ return null;
703
+ }
704
+
705
+ /* One probe at mount is not enough. On a page whose stylesheets are injected
706
+ by script (Tilda is the reported case) a CACHED reload mounts the banner
707
+ BEFORE those sheets apply, so every sample still computes to the UA default
708
+ and the probe returns null — the banner stays in Times and nothing looks
709
+ again. So the mount probe is only the first look: re-probes are scheduled
710
+ until the answer stops changing.
711
+
712
+ Both halves of that schedule are pure arithmetic, published on _contrast so
713
+ the behaviour is testable without timers or a DOM.
714
+
715
+ The timed ladder, in ms after mount. Deliberately front-loaded: a page that
716
+ styles itself quickly is corrected before a visitor can read the banner,
717
+ and the 4 s tail catches a slow webfont-driven sheet. `window.load` and
718
+ `document.fonts.ready` fire on their own and are NOT part of this ladder. */
719
+ var REPROBE_DELAYS = [500, 1500, 4000];
720
+
721
+ // Attempt is 1-based over the TIMED probes only; null means "no more timers".
722
+ function nextProbeDelay(attempt) {
723
+ if (typeof attempt !== 'number' || !isFinite(attempt) || attempt < 1) return null;
724
+ var i = Math.floor(attempt) - 1;
725
+ return i < REPROBE_DELAYS.length ? REPROBE_DELAYS[i] : null;
726
+ }
727
+
728
+ /* Should a freshly probed family replace what the banner is painting?
729
+ `current` is the applied value (null while the page still states nothing).
730
+ Only a non-null find is ever worth applying — a later null means the page
731
+ got LESS specific, which never happens for real and would otherwise throw
732
+ away a good answer. */
733
+ function shouldReprobe(current, found) {
734
+ if (typeof found !== 'string' || !found) return false;
735
+ if (typeof current !== 'string' || !current) return true; // null -> found
736
+ return normFamily(current) !== normFamily(found);
737
+ }
738
+
739
+ // Ordered so the first hit is the most representative body text on the page.
740
+ var FONT_PROBES = ['main p', 'article p', '[role=main] p', 'p', 'h1', 'h2', 'a', 'button', 'li'];
741
+
742
+ function visibleOutsideHost(el) {
743
+ if (!el) return false;
744
+ if (host && (el === host || (host.contains && host.contains(el)))) return false;
745
+ try {
746
+ if (el.offsetParent !== null) return true;
747
+ return !!(el.getClientRects && el.getClientRects().length > 0);
748
+ } catch (e) { return false; }
749
+ }
750
+
751
+ // The UA default is whatever an element states no opinion about, measured
752
+ // INSIDE the shadow root so `:host{all:initial}` is already in force.
753
+ function uaDefaultFamily() {
754
+ if (!root || typeof document === 'undefined') return '';
755
+ var probe = null;
756
+ try {
757
+ probe = document.createElement('span');
758
+ probe.style.setProperty('font-family', 'initial');
759
+ root.appendChild(probe);
760
+ return window.getComputedStyle(probe).fontFamily || '';
761
+ } catch (e) {
762
+ return '';
763
+ } finally {
764
+ try { if (probe && probe.parentNode) probe.parentNode.removeChild(probe); } catch (e2) { /* noop */ }
765
+ }
766
+ }
767
+
768
+ // Cached at applyTheme time so the debug panel (which may open long before or
769
+ // after a remount) quotes the value the banner actually painted.
770
+ var pageFont = null;
771
+
772
+ /* How many times the SCHEDULER has looked, for the debug panel's «попытка N».
773
+ Counted here and not inside resolvePageFont() on purpose: _resolvePageFont()
774
+ re-runs the whole probe on every debug-panel render, and counting there would
775
+ report «попытка 14» after a few renders and exhaust the cap on nothing. */
776
+ var fontProbeCount = 1; // the mount probe is attempt 1
777
+
778
+ function resolvePageFont() {
779
+ if (typeof document === 'undefined') return pageFont;
780
+ try {
781
+ var ua = uaDefaultFamily();
782
+ var samples = [];
783
+ var body = document.body;
784
+ if (body) {
785
+ try { samples.push({ family: window.getComputedStyle(body).fontFamily }); } catch (e) { /* noop */ }
786
+ }
787
+ for (var i = 0; i < FONT_PROBES.length; i++) {
788
+ // First VISIBLE match, not the first match if it happens to be visible:
789
+ // the leading <p> on a real page is often hidden a11y text, a template
790
+ // or an inert modal, and skipping straight to the next selector would
791
+ // throw away the page's body copy. Capped so a long article does not
792
+ // cost a layout read per node.
793
+ var el = null, list = null;
794
+ try { list = document.querySelectorAll(FONT_PROBES[i]); } catch (e) { list = null; }
795
+ if (!list) continue;
796
+ for (var j = 0; j < list.length && j < 5; j++) {
797
+ if (visibleOutsideHost(list[j])) { el = list[j]; break; }
798
+ }
799
+ if (!el) continue;
800
+ try { samples.push({ family: window.getComputedStyle(el).fontFamily }); } catch (e) { /* noop */ }
801
+ }
802
+ pageFont = pickPageFont(samples, ua);
803
+ } catch (e) {
804
+ pageFont = null;
805
+ }
806
+ return pageFont;
807
+ }
808
+
809
+ /* ------------------------------------------------ page-font re-probing */
810
+
811
+ /* Live timers, so remount() can cancel a schedule that belongs to the DOM it
812
+ is about to throw away. Everything here is guarded and only ever RUNS after
813
+ mount(), so the parse-time SSR context below never reaches it. */
814
+ var fontTimers = [];
815
+ var fontStable = false; // a non-null value survived one extra probe
816
+ var fontScheduled = false;
817
+
818
+ // Cap on SCHEDULED probes (the mount probe is not one of them): the three
819
+ // timed rungs plus window.load plus fonts.ready.
820
+ var MAX_REPROBES = 5;
821
+
822
+ function clearFontTimers() {
823
+ for (var i = 0; i < fontTimers.length; i++) {
824
+ try { clearTimeout(fontTimers[i]); } catch (e) { /* noop */ }
825
+ }
826
+ fontTimers = [];
827
+ }
828
+
829
+ /* One look. Re-applies the theme when the family changed, and reports whether
830
+ the answer has now held still for a whole extra probe — which is the only
831
+ thing that stops the ladder early. Applying and stopping are deliberately
832
+ separate: a first non-null find is painted IMMEDIATELY (the visitor must not
833
+ wait out the confirmation), the confirmation only decides about timers. */
834
+ function probeFontOnce() {
835
+ var before = pageFont;
836
+ var found = resolvePageFont(); // rewrites the pageFont cache
837
+ if (shouldReprobe(before, found)) {
838
+ // applyTheme() rewrites themeStyle.textContent wholesale, so the --ck-font
839
+ // rule is REPLACED rather than appended a second time.
840
+ try { applyTheme(safeConfig()); } catch (e) { /* noop */ }
841
+ fontStable = false;
842
+ return false;
843
+ }
844
+ // Unchanged. Stable only once we have an actual family in hand: a page that
845
+ // keeps answering null is exactly the case that has to keep looking.
846
+ if (found) { fontStable = true; return true; }
847
+ return false;
848
+ }
849
+
850
+ /* Run a scheduled probe. Every trigger — window.load, fonts.ready and each
851
+ rung of the timed ladder — funnels through here so the cap and the counter
852
+ are counted in one place. */
853
+ function runScheduledProbe() {
854
+ if (!mounted || fontStable) return;
855
+ if (fontProbeCount >= 1 + MAX_REPROBES) return;
856
+ fontProbeCount++;
857
+ if (probeFontOnce()) clearFontTimers(); // settled: drop the rest
858
+ }
859
+
860
+ /* Called once from the end of mount(). Not from applyTheme(): that runs again
861
+ on every palette-only ck:init, which would restart the whole ladder. */
862
+ function scheduleFontProbes(cfg) {
863
+ if (typeof window === 'undefined' || typeof document === 'undefined') return;
864
+ if (fontScheduled) return;
865
+ // theme.font:'system' is a fixed stack — there is nothing to sample.
866
+ try {
867
+ var t = (cfg && cfg.theme) || {};
868
+ if (resolveFont(t) !== 'inherit') return;
869
+ } catch (e) { return; }
870
+
871
+ fontScheduled = true;
872
+ try {
873
+ for (var a = 1; ; a++) {
874
+ var d = nextProbeDelay(a);
875
+ if (d === null) break;
876
+ fontTimers.push(setTimeout(runScheduledProbe, d));
877
+ }
878
+ } catch (e) { /* noop */ }
879
+
880
+ // A cached reload's mount can precede the page's own stylesheets; `load`
881
+ // is the first moment every <link> in the document has definitely applied.
882
+ try {
883
+ if (document.readyState === 'complete') {
884
+ // Already past it — the ladder covers this case on its own.
885
+ } else {
886
+ window.addEventListener('load', function () { runScheduledProbe(); }, { once: true });
887
+ }
888
+ } catch (e) { /* noop */ }
889
+
890
+ // A webfont swapping in can change the computed family outright.
891
+ try {
892
+ if (document.fonts && typeof document.fonts.ready === 'object' &&
893
+ document.fonts.ready && typeof document.fonts.ready.then === 'function') {
894
+ document.fonts.ready.then(function () { runScheduledProbe(); },
895
+ function () { /* noop */ });
896
+ }
897
+ } catch (e) { /* noop */ }
898
+ }
899
+
666
900
  /* --------------------------------------------------- button resolution */
667
901
 
668
902
  var BTN_ROLES = ['accept', 'reject', 'settings'];
@@ -919,9 +1153,25 @@
919
1153
  function applyTheme(cfg) {
920
1154
  if (!host || !nodes.themeStyle) return;
921
1155
  var built = buildThemeCss(cfg);
1156
+ // `inherit` would take the family of the host <div> — i.e. of <body>, which
1157
+ // on many sites still carries the browser default. Overwrite the token with
1158
+ // the family real page text is set in. Appended AFTER built.css so the two
1159
+ // equal-specificity :host rules resolve in source order; buildThemeCss stays
1160
+ // pure and DOM-free (the theme editor and the debug panel both call it).
1161
+ var fontCss = '';
1162
+ if (built.font === 'inherit') {
1163
+ var page = resolvePageFont();
1164
+ // Owner rule (2026-09-06): the page's font when it can be read, the
1165
+ // system stack when it cannot — never a bare `inherit`, which on Tilda
1166
+ // and friends resolves to <body>'s browser default (Times). A later
1167
+ // re-probe that finds the page font swaps the system stack out again.
1168
+ fontCss = '\n:host{--ck-font:' + (page || SYSTEM_FONT) + '}';
1169
+ } else {
1170
+ pageFont = null; // theme.font:'system' opts out
1171
+ }
922
1172
  // Brand rules ride along in the same sheet: the dark/light logo swap depends
923
1173
  // on theme.mode, so it must be rebuilt whenever the palette is.
924
- nodes.themeStyle.textContent = built.css + '\n' + buildBrandCss(cfg); // replace, never append
1174
+ nodes.themeStyle.textContent = built.css + fontCss + '\n' + buildBrandCss(cfg); // replace, never append
925
1175
  host.classList.remove('ck-mode-dark', 'ck-mode-light');
926
1176
  if (built.mode === 'dark') host.classList.add('ck-mode-dark');
927
1177
  else if (built.mode === 'light') host.classList.add('ck-mode-light');
@@ -1292,6 +1542,21 @@
1292
1542
 
1293
1543
  function openPanel(invoker) {
1294
1544
  if (!mounted || !nodes.panel) return;
1545
+ /* A visitor who opens the settings long after load is the last chance to
1546
+ get the typeface right, and by then the page is certainly styled. Only
1547
+ when the banner is still on `inherit` — a resolved family is left alone
1548
+ so opening the panel can never restyle a correct banner. Hooked here
1549
+ rather than on the ck:ui:open-preferences listener because the banner's
1550
+ own «Настроить» button calls openPanel() directly.
1551
+
1552
+ Gated on fontScheduled, which is only ever set for a theme that actually
1553
+ samples the page: with theme.font:'system' applyTheme() nulls pageFont on
1554
+ every call, so without this the condition would be permanently true and
1555
+ every panel open would re-probe the DOM and rewrite the sheet to produce
1556
+ byte-identical CSS, forever. */
1557
+ if (fontScheduled && !pageFont && !fontStable) {
1558
+ try { probeFontOnce(); } catch (e) { /* noop */ }
1559
+ }
1295
1560
  lastFocus = invoker || root.activeElement || document.activeElement;
1296
1561
  syncSwitches(safeState());
1297
1562
  nodes.panelScrim.classList.remove('ck-hidden');
@@ -1367,6 +1632,12 @@
1367
1632
  mounted = false;
1368
1633
  panelOpen = false;
1369
1634
  lastFocus = null;
1635
+ // The pending schedule belongs to the shadow root about to be rebuilt; the
1636
+ // fresh mount() starts its own ladder from a clean count.
1637
+ clearFontTimers();
1638
+ fontScheduled = false;
1639
+ fontStable = false;
1640
+ fontProbeCount = 1;
1370
1641
  mount(cfg);
1371
1642
  }
1372
1643
 
@@ -1411,6 +1682,10 @@
1411
1682
  mounted = true;
1412
1683
  mountedSig = signature(cfg);
1413
1684
  syncFromState();
1685
+
1686
+ // Only now: scheduleFontProbes() re-applies the theme from a timer, and
1687
+ // applyTheme() is a no-op until there is a host and a themeStyle to write.
1688
+ scheduleFontProbes(cfg);
1414
1689
  }
1415
1690
 
1416
1691
  /* ---------------------------------------------------------------- events */
@@ -1437,9 +1712,23 @@
1437
1712
  resolveButtonStyles: resolveButtonStyles,
1438
1713
  resolveRadius: resolveRadius,
1439
1714
  resolveFont: resolveFont,
1715
+ pickPageFont: pickPageFont,
1716
+ nextProbeDelay: nextProbeDelay,
1717
+ shouldReprobe: shouldReprobe,
1440
1718
  resolveDetails: resolveDetails,
1441
1719
  buildThemeCss: buildThemeCss
1442
1720
  };
1721
+ // The page-font probe reads the DOM, so it is not part of the pure block —
1722
+ // but the debug panel must be able to quote the family the banner painted
1723
+ // rather than guess at it. Safe to call before mount: with no shadow root
1724
+ // to measure the UA default against it simply returns the cache (null).
1725
+ ck._resolvePageFont = function () {
1726
+ return (typeof document === 'undefined' || !root) ? pageFont : resolvePageFont();
1727
+ };
1728
+ // How many times the SCHEDULER has looked (1 = the mount probe alone). The
1729
+ // debug panel prints it: on a Tilda-style page the interesting fact is that
1730
+ // the family arrived on the third look, not on the first.
1731
+ ck._pageFontAttempt = function () { return fontProbeCount; };
1443
1732
  })();
1444
1733
 
1445
1734
  // SSR-safe: with no DOM there is nothing to render or listen to, so importing