@ecomconsult/consentkit 0.3.4 → 0.4.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/README.md +260 -16
- package/npm/core.cjs +1 -1
- package/npm/core.mjs +4 -1
- package/npm/index.cjs +5 -2
- package/npm/index.d.ts +68 -0
- package/npm/index.mjs +14 -2
- package/npm/internal-stub.mjs +7 -1
- package/package.json +1 -1
- package/src/ck-core.js +488 -29
- package/src/ck-debug-loader.js +134 -0
- package/src/ck-debug.js +862 -0
- package/src/ck-saas.js +36 -1
- package/src/ck-ui-branding.js +511 -0
- package/src/ck-ui.js +43 -416
package/src/ck-ui.js
CHANGED
|
@@ -228,13 +228,6 @@
|
|
|
228
228
|
'border:1px solid var(--ck-line);border-radius:var(--ck-radius);overflow:hidden}',
|
|
229
229
|
'.ck-panel__head{display:flex;align-items:flex-start;gap:16px;padding:22px 24px 14px;',
|
|
230
230
|
'border-bottom:1px solid var(--ck-line)}',
|
|
231
|
-
/* min-width:0 so a wide logo shrinks rather than shoving the close button
|
|
232
|
-
off. Scoped to :has(.ck-brand) — applying it unconditionally changes the
|
|
233
|
-
header block's flex sizing (480px -> 518px) on unbranded panels too, which
|
|
234
|
-
would break byte-for-byte backward compatibility. Browsers without :has()
|
|
235
|
-
simply keep today's sizing; the logo is width-capped at 160px regardless,
|
|
236
|
-
so the close button still has room. */
|
|
237
|
-
'.ck-panel__head>div:first-child:has(.ck-brand){flex:1 1 auto;min-width:0}',
|
|
238
231
|
'.ck-panel__head h2{margin:0 0 4px;font-size:18px;font-weight:600;letter-spacing:-.01em}',
|
|
239
232
|
'.ck-panel__head p{margin:0;font-size:14px;color:var(--ck-muted)}',
|
|
240
233
|
'.ck-x{flex:none;width:36px;height:36px;border-radius:var(--ck-radius);border:1px solid var(--ck-line);',
|
|
@@ -279,59 +272,6 @@
|
|
|
279
272
|
'td.ck-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}',
|
|
280
273
|
'.ck-empty{margin:8px 0 0;font-size:13px;color:var(--ck-muted)}',
|
|
281
274
|
|
|
282
|
-
/* ---- branding: logo + powered-by ----
|
|
283
|
-
The logo sits inline with the title in a flex row. That was chosen over a
|
|
284
|
-
separate band above the heading because .ck-banner--bar is a single
|
|
285
|
-
vertically-centred flex row: a stacked logo adds a height band there and
|
|
286
|
-
nowhere else, so bar/box/modal would drift apart. Inline keeps one rule
|
|
287
|
-
for all three layouts and leaves existing margins untouched — .ck-brand
|
|
288
|
-
carries the whole gap, h2 keeps its own margin. */
|
|
289
|
-
'.ck-brand{display:flex;align-items:center;gap:10px;margin:0 0 8px}',
|
|
290
|
-
/* Attribution foot of the banner: logo + credit on one muted line below
|
|
291
|
-
the buttons. flex-basis 100% keeps it on its own row in the bar layout,
|
|
292
|
-
where the actions sit beside the text.
|
|
293
|
-
|
|
294
|
-
The mark is desaturated here rather than shipped as a second grey asset:
|
|
295
|
-
an agency logo in full brand colour reads as a second call to action
|
|
296
|
-
competing with the consent buttons. grayscale() flattens the hue and the
|
|
297
|
-
opacity lifts it off pure black, so it sits at signature weight in both
|
|
298
|
-
themes without the integrator preparing anything. */
|
|
299
|
-
'.ck-foot{display:flex;align-items:center;gap:8px;flex-wrap:wrap;',
|
|
300
|
-
'margin:14px 0 0}',
|
|
301
|
-
'.ck-foot .ck-brand__logo,.ck-foot .ck-brand__logo svg{',
|
|
302
|
-
'filter:grayscale(1);opacity:.55}',
|
|
303
|
-
'.ck-foot .ck-brand__link:hover .ck-brand__logo,',
|
|
304
|
-
'.ck-foot .ck-brand__link:focus-visible .ck-brand__logo{opacity:.8}',
|
|
305
|
-
'.ck-foot .ck-brand{margin:0}',
|
|
306
|
-
/* Beats the flex:1 1 100% the standalone .ck-powered carries (it needs a
|
|
307
|
-
full row of its own when there is no logo beside it). */
|
|
308
|
-
'.ck-foot p.ck-powered,.ck-banner .ck-foot p.ck-powered{margin:0;flex:0 1 auto}',
|
|
309
|
-
/* In the panel the foot shares a flex row with the action buttons, so it
|
|
310
|
-
claims a row of its own below them. */
|
|
311
|
-
'.ck-panel__foot .ck-foot{flex:1 1 100%;margin:2px 0 0}',
|
|
312
|
-
'.ck-brand__logo{display:block;width:auto;max-width:160px;height:var(--ck-logo-h,24px);',
|
|
313
|
-
'flex:none;object-fit:contain}',
|
|
314
|
-
'.ck-brand__logo svg{display:block;width:auto;height:100%;max-width:160px}',
|
|
315
|
-
'.ck-brand a.ck-brand__link{display:inline-flex;align-items:center;text-decoration:none;flex:none}',
|
|
316
|
-
/* Dark-variant swap is CSS-driven, mirroring buildThemeCss()'s cascade
|
|
317
|
-
exactly (same three selectors, same :not(.ck-mode-light) guard). Reading
|
|
318
|
-
the theme in JS would desync in auto mode and would not follow a live
|
|
319
|
-
system theme flip. */
|
|
320
|
-
'.ck-brand__dark{display:none}',
|
|
321
|
-
'.ck-brand__has-dark .ck-brand__light{display:block}',
|
|
322
|
-
|
|
323
|
-
/* ---- powered-by ----
|
|
324
|
-
Deliberately quiet: muted colour, 12px, normal weight, and it comes after
|
|
325
|
-
the action row in DOM order. It must not compete with the consent buttons. */
|
|
326
|
-
/* .ck-banner p sets font-size:14px at equal specificity and appears later in
|
|
327
|
-
this sheet, so it would win over a bare .ck-powered. Qualifying the
|
|
328
|
-
selector keeps the attribution smaller than the button text (14px) without
|
|
329
|
-
reaching for !important. */
|
|
330
|
-
'.ck-powered,.ck-banner p.ck-powered{margin:12px 0 0;font-size:12px;line-height:1.4;',
|
|
331
|
-
'color:var(--ck-muted);flex:1 1 100%;font-weight:400}',
|
|
332
|
-
'.ck-powered a{color:var(--ck-muted);text-decoration:underline}',
|
|
333
|
-
'.ck-panel__foot .ck-powered{margin:0;align-self:center}',
|
|
334
|
-
|
|
335
275
|
/* ---- floating button ---- */
|
|
336
276
|
'.ck-fab{position:fixed;left:16px;bottom:16px;z-index:2147482999;width:48px;height:48px;',
|
|
337
277
|
'border-radius:50%;border:1px solid var(--ck-line);background:var(--ck-bg);color:var(--ck-accent);',
|
|
@@ -342,15 +282,10 @@
|
|
|
342
282
|
'.ck-banner--bar{left:8px;right:8px;bottom:8px;padding:16px}',
|
|
343
283
|
'.ck-actions{width:100%}.ck-btn{min-width:0;flex:1 1 100%}',
|
|
344
284
|
'.ck-banner.ck-banner--bar p{margin-bottom:22px}',
|
|
345
|
-
/* Narrow bar stacks into a column, so the foot — which lives at the end of
|
|
346
|
-
the text block for the wide side-by-side layout — would sit between the
|
|
347
|
-
question and the buttons answering it. Lift it out of the text block and
|
|
348
|
-
order it last. */
|
|
349
285
|
'.ck-banner--bar{flex-direction:column;align-items:stretch}',
|
|
350
286
|
'.ck-banner--bar .ck-banner__body{display:contents}',
|
|
351
287
|
'.ck-banner--bar .ck-banner__body>*{order:1}',
|
|
352
|
-
'.ck-banner--bar .ck-actions{order:2}',
|
|
353
|
-
'.ck-banner--bar .ck-foot{order:3;margin-top:14px}}',
|
|
288
|
+
'.ck-banner--bar .ck-actions{order:2}}',
|
|
354
289
|
|
|
355
290
|
'@media (prefers-reduced-motion: no-preference){',
|
|
356
291
|
'.ck-btn,.ck-x,.ck-fab,.ck-switch,.ck-switch::after{transition:background-color .16s ease,border-color .16s ease,color .16s ease,left .16s ease,right .16s ease}}'
|
|
@@ -376,350 +311,55 @@
|
|
|
376
311
|
return (typeof window !== 'undefined' && window.ConsentKit) || null;
|
|
377
312
|
}
|
|
378
313
|
|
|
379
|
-
/*
|
|
380
|
-
|
|
381
|
-
/* Restraint is the design rule here, not a matter of taste.
|
|
382
|
-
|
|
383
|
-
A consent banner is shown to every visitor of the site that installs it,
|
|
384
|
-
and it asks them a legal question. An agency logo, agency colours and an
|
|
385
|
-
attribution line all at once make it read as the agency's dialogue rather
|
|
386
|
-
than the site's own — visitors trust it less, and the banner competes with
|
|
387
|
-
the page it sits on.
|
|
388
|
-
|
|
389
|
-
So: everything in `branding` is off unless asked for, and the recommended
|
|
390
|
-
shape is one small logo (16–20px) OR one attribution line — with
|
|
391
|
-
theme.accent left matching the HOST SITE, never the agency's colour.
|
|
392
|
-
Nothing here may outweigh the consent buttons. */
|
|
393
|
-
|
|
394
|
-
/* SVG sanitiser.
|
|
395
|
-
|
|
396
|
-
branding.logo may be a raw SVG string coming from a server-rendered config
|
|
397
|
-
or a WordPress admin field. That is untrusted input, so it never reaches
|
|
398
|
-
innerHTML: `<svg onload=...>` executes on insertion, and so do SMIL
|
|
399
|
-
`<animate onbegin=...>` and `<foreignObject><img onerror=...>`.
|
|
400
|
-
|
|
401
|
-
Approach chosen: parse inert, then REBUILD rather than strip-and-adopt.
|
|
402
|
-
DOMParser with 'image/svg+xml' yields a detached, non-live document where
|
|
403
|
-
nothing runs. We then walk that tree and construct a brand-new tree with
|
|
404
|
-
createElementNS, copying across only allowlisted tags and attributes.
|
|
405
|
-
|
|
406
|
-
Rebuilding is what makes this safe rather than merely careful. The rejected
|
|
407
|
-
alternative — importNode/appendChild the parsed tree after deleting bad
|
|
408
|
-
attributes — arms every inline handler at the moment of adoption, so a
|
|
409
|
-
single missed attribute name is live code. Here an attribute we do not
|
|
410
|
-
recognise is simply never written, so the failure mode is a missing
|
|
411
|
-
decoration, not script execution. Allowlists (closed) beat blocklists
|
|
412
|
-
(open-ended) for the same reason.
|
|
413
|
-
|
|
414
|
-
Deliberately excluded, each for a concrete reason:
|
|
415
|
-
script - obvious
|
|
416
|
-
foreignObject - escape hatch back into full HTML
|
|
417
|
-
use, image - can reference/fetch external documents
|
|
418
|
-
a - javascript: navigation inside the logo
|
|
419
|
-
style - CSS escapes, and it would leak out of the
|
|
420
|
-
logo into our own shadow-root styling
|
|
421
|
-
animate/set/animateTransform - SMIL takes an attributeName and can drive
|
|
422
|
-
arbitrary attributes, plus on* timing events
|
|
423
|
-
|
|
424
|
-
Anything unexpected bails to null (no logo) rather than partially rendering. */
|
|
425
|
-
|
|
426
|
-
var SVG_NS = 'http://www.w3.org/2000/svg';
|
|
427
|
-
|
|
428
|
-
var SVG_TAGS = {
|
|
429
|
-
svg: 1, g: 1, path: 1, circle: 1, ellipse: 1, rect: 1, line: 1,
|
|
430
|
-
polyline: 1, polygon: 1, defs: 1, title: 1, desc: 1,
|
|
431
|
-
lineargradient: 1, radialgradient: 1, stop: 1, clippath: 1, mask: 1
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
// Presentation/geometry only. No href/xlink:href in any form, no on* events.
|
|
435
|
-
var SVG_ATTRS = {
|
|
436
|
-
viewbox: 'viewBox', preserveaspectratio: 'preserveAspectRatio',
|
|
437
|
-
xmlns: 'xmlns', version: 'version',
|
|
438
|
-
d: 'd', fill: 'fill', 'fill-rule': 'fill-rule', 'fill-opacity': 'fill-opacity',
|
|
439
|
-
'clip-rule': 'clip-rule', 'clip-path': 'clip-path', mask: 'mask',
|
|
440
|
-
stroke: 'stroke', 'stroke-width': 'stroke-width', 'stroke-linecap': 'stroke-linecap',
|
|
441
|
-
'stroke-linejoin': 'stroke-linejoin', 'stroke-dasharray': 'stroke-dasharray',
|
|
442
|
-
'stroke-dashoffset': 'stroke-dashoffset', 'stroke-opacity': 'stroke-opacity',
|
|
443
|
-
'stroke-miterlimit': 'stroke-miterlimit',
|
|
444
|
-
opacity: 'opacity', transform: 'transform',
|
|
445
|
-
x: 'x', y: 'y', x1: 'x1', y1: 'y1', x2: 'x2', y2: 'y2',
|
|
446
|
-
cx: 'cx', cy: 'cy', r: 'r', rx: 'rx', ry: 'ry',
|
|
447
|
-
width: 'width', height: 'height', points: 'points',
|
|
448
|
-
offset: 'offset', 'stop-color': 'stop-color', 'stop-opacity': 'stop-opacity',
|
|
449
|
-
gradientunits: 'gradientUnits', gradienttransform: 'gradientTransform',
|
|
450
|
-
spreadmethod: 'spreadMethod', clippathunits: 'clipPathUnits',
|
|
451
|
-
maskunits: 'maskUnits', maskcontentunits: 'maskContentUnits',
|
|
452
|
-
id: 'id', 'class': 'class'
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
/* Every sanitised logo gets a unique id namespace.
|
|
456
|
-
|
|
457
|
-
buildBrandLogo() runs twice per mount (banner + panel head), and doubles
|
|
458
|
-
again when logoDark is set — so one shadow root can hold four copies of the
|
|
459
|
-
same asset. A gradient/clipPath/mask id like "g" would then appear four
|
|
460
|
-
times, and url(#g) resolves to the FIRST match in the tree: the dark logo
|
|
461
|
-
would silently paint with the light logo's gradient stops. Prefixing every
|
|
462
|
-
id per instance, and rewriting the url(#…) references in the same pass,
|
|
463
|
-
keeps each copy self-contained. */
|
|
464
|
-
var svgSeq = 0;
|
|
465
|
-
|
|
466
|
-
// url(#localRef) and plain values only — no url(http…), no javascript:.
|
|
467
|
-
// `prefix` namespaces id definitions and their url(#…) references together.
|
|
468
|
-
function safeAttrValue(name, value, prefix) {
|
|
469
|
-
var v = String(value == null ? '' : value);
|
|
470
|
-
// Strip nothing; reject outright. Control chars are how javascript: is hidden.
|
|
471
|
-
var probe = v.replace(/[\u0000-\u0020\u007f-\u00a0]/g, '').toLowerCase();
|
|
472
|
-
if (probe.indexOf('javascript:') !== -1) return null;
|
|
473
|
-
if (probe.indexOf('data:text') !== -1) return null;
|
|
474
|
-
if (probe.indexOf('&#') !== -1) return null;
|
|
475
|
-
// Any url() must be a same-document fragment reference.
|
|
476
|
-
if (probe.indexOf('url(') !== -1 && !/^url\(#[a-z0-9_.:-]+\)$/i.test(probe)) return null;
|
|
477
|
-
if (name === 'id') {
|
|
478
|
-
if (!/^[a-zA-Z][\w.:-]*$/.test(v)) return null;
|
|
479
|
-
return prefix + v;
|
|
480
|
-
}
|
|
481
|
-
// Rewrite a reference so it points at THIS instance's namespaced definition.
|
|
482
|
-
var m = /^url\(#([\w.:-]+)\)$/.exec(v);
|
|
483
|
-
if (m) return 'url(#' + prefix + m[1] + ')';
|
|
484
|
-
return v;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function rebuildSvgNode(src, out, depth, prefix) {
|
|
488
|
-
if (depth > 24) return false; // pathological nesting
|
|
489
|
-
var kids = src.childNodes;
|
|
490
|
-
for (var i = 0; i < kids.length; i++) {
|
|
491
|
-
var n = kids[i];
|
|
492
|
-
if (n.nodeType === 3) { // text (only inside title/desc)
|
|
493
|
-
var pt = out.nodeName.toLowerCase();
|
|
494
|
-
if (pt === 'title' || pt === 'desc') out.appendChild(document.createTextNode(n.nodeValue));
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
if (n.nodeType !== 1) continue; // drop comments, CDATA, PIs
|
|
498
|
-
var tag = String(n.nodeName || '').toLowerCase();
|
|
499
|
-
if (!Object.prototype.hasOwnProperty.call(SVG_TAGS, tag)) return false; // bail, don't skip
|
|
500
|
-
var fresh = document.createElementNS(SVG_NS, n.nodeName);
|
|
501
|
-
var attrs = n.attributes || [];
|
|
502
|
-
for (var a = 0; a < attrs.length; a++) {
|
|
503
|
-
var an = String(attrs[a].name || '').toLowerCase();
|
|
504
|
-
if (/^on/i.test(an)) return false; // event handler present -> reject whole logo
|
|
505
|
-
if (an === 'href' || an === 'xlink:href' || an.indexOf('xlink') === 0) return false;
|
|
506
|
-
if (!Object.prototype.hasOwnProperty.call(SVG_ATTRS, an)) continue; // unknown -> just omit
|
|
507
|
-
var val = safeAttrValue(an, attrs[a].value, prefix);
|
|
508
|
-
if (val === null) continue;
|
|
509
|
-
try { fresh.setAttribute(SVG_ATTRS[an], val); } catch (e) { /* ignore */ }
|
|
510
|
-
}
|
|
511
|
-
if (!rebuildSvgNode(n, fresh, depth + 1, prefix)) return false;
|
|
512
|
-
out.appendChild(fresh);
|
|
513
|
-
}
|
|
514
|
-
return true;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Raw SVG string -> freshly built, safe <svg> element, or null.
|
|
518
|
-
function sanitizeSvg(markup) {
|
|
519
|
-
var s = str(markup);
|
|
520
|
-
if (!s || s.length > 512 * 1024) return null;
|
|
521
|
-
if (!/^\s*<svg[\s>]/i.test(s)) return null; // must be an SVG root
|
|
522
|
-
var doc;
|
|
523
|
-
try {
|
|
524
|
-
doc = new DOMParser().parseFromString(s, 'image/svg+xml');
|
|
525
|
-
} catch (e) { return null; }
|
|
526
|
-
if (!doc) return null;
|
|
527
|
-
if (doc.getElementsByTagName('parsererror').length) return null;
|
|
528
|
-
var srcRoot = doc.documentElement;
|
|
529
|
-
if (!srcRoot || String(srcRoot.nodeName).toLowerCase() !== 'svg') return null;
|
|
530
|
-
|
|
531
|
-
// Unique per sanitised instance, so four copies of one asset never collide.
|
|
532
|
-
var prefix = 'ck' + (++svgSeq) + '-';
|
|
533
|
-
|
|
534
|
-
var svg = document.createElementNS(SVG_NS, 'svg');
|
|
535
|
-
var ra = srcRoot.attributes || [];
|
|
536
|
-
for (var i = 0; i < ra.length; i++) {
|
|
537
|
-
var an = String(ra[i].name || '').toLowerCase();
|
|
538
|
-
if (/^on/i.test(an)) return null;
|
|
539
|
-
if (an.indexOf('xlink') === 0 || an === 'href') return null;
|
|
540
|
-
if (!Object.prototype.hasOwnProperty.call(SVG_ATTRS, an)) continue;
|
|
541
|
-
var val = safeAttrValue(an, ra[i].value, prefix);
|
|
542
|
-
if (val === null) continue;
|
|
543
|
-
try { svg.setAttribute(SVG_ATTRS[an], val); } catch (e) {}
|
|
544
|
-
}
|
|
545
|
-
if (!rebuildSvgNode(srcRoot, svg, 0, prefix)) return null;
|
|
546
|
-
return svg;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/* Image-source logos.
|
|
550
|
-
Only http(s) and image data: URIs. data:text/html is a navigation/XSS
|
|
551
|
-
vector via <img>-adjacent contexts, and any other scheme is rejected.
|
|
552
|
-
|
|
553
|
-
NOTE FOR INTEGRATORS: an https:// logo is an external network request that
|
|
554
|
-
fires BEFORE the visitor has consented to anything. It leaks IP, User-Agent
|
|
555
|
-
and Referer to whoever hosts the file. ConsentKit therefore recommends an
|
|
556
|
-
inline SVG string or a data: URI, both of which are entirely local. An
|
|
557
|
-
external URL still works — it is the integrator's call, made knowingly —
|
|
558
|
-
and we send referrerpolicy=no-referrer to reduce what leaks. */
|
|
559
|
-
function safeImgSrc(value) {
|
|
560
|
-
var v = str(value);
|
|
561
|
-
if (!v) return null;
|
|
562
|
-
if (/^https?:\/\//i.test(v)) return v;
|
|
563
|
-
if (/^data:image\/(svg\+xml|png|jpe?g|webp|gif|avif)[;,]/i.test(v)) return v;
|
|
564
|
-
return null;
|
|
565
|
-
}
|
|
314
|
+
/* ------------------------------------------------- branding (optional file) */
|
|
566
315
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
if (!isFinite(n)) return 18;
|
|
572
|
-
if (n < 14) return 14;
|
|
573
|
-
if (n > 32) return 32;
|
|
574
|
-
return Math.round(n);
|
|
575
|
-
}
|
|
316
|
+
/* Branding lives in src/ck-ui-branding.js and is OPTIONAL. Everything below
|
|
317
|
+
asks for the extension and does nothing when it is absent, so a build
|
|
318
|
+
without that file draws no logo, no attribution line and no branding CSS —
|
|
319
|
+
and errors nowhere. See that file's header for the load-order contract.
|
|
576
320
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
var b = cfg && cfg.branding;
|
|
586
|
-
return (b && typeof b === 'object' && !Array.isArray(b)) ? b : null;
|
|
321
|
+
The extension is handed `host` on every DOM-producing call: ck-ui owns el()
|
|
322
|
+
and the localised strings (T is reassigned per mount), so passing them in
|
|
323
|
+
keeps one dictionary and one set of helpers rather than two copies. */
|
|
324
|
+
function brandingExt() {
|
|
325
|
+
var ck = api();
|
|
326
|
+
var ext = ck && ck._uiExtensions;
|
|
327
|
+
var b = ext && ext.branding;
|
|
328
|
+
return (b && typeof b === 'object') ? b : null;
|
|
587
329
|
}
|
|
588
330
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if (!source) return null;
|
|
592
|
-
var node = null;
|
|
593
|
-
var s = str(source);
|
|
594
|
-
if (!s) return null;
|
|
595
|
-
|
|
596
|
-
if (/^\s*</.test(s)) {
|
|
597
|
-
node = sanitizeSvg(s); // raw markup -> rebuilt SVG
|
|
598
|
-
if (node) node.classList.add('ck-brand__logo');
|
|
599
|
-
} else {
|
|
600
|
-
var src = safeImgSrc(s);
|
|
601
|
-
if (!src) return null;
|
|
602
|
-
node = el('img', 'ck-brand__logo');
|
|
603
|
-
node.setAttribute('referrerpolicy', 'no-referrer');
|
|
604
|
-
node.setAttribute('decoding', 'async');
|
|
605
|
-
node.src = src;
|
|
606
|
-
node.alt = decorative ? '' : (alt || '');
|
|
607
|
-
}
|
|
608
|
-
// The SVG carries no accessible name of its own; the wrapper supplies one
|
|
609
|
-
// (or hides it, when a sibling already names the logo).
|
|
610
|
-
if (node && node.nodeName.toLowerCase() === 'svg') {
|
|
611
|
-
node.setAttribute('aria-hidden', 'true');
|
|
612
|
-
node.setAttribute('focusable', 'false');
|
|
613
|
-
}
|
|
614
|
-
return node;
|
|
331
|
+
function extHost() {
|
|
332
|
+
return { el: el, str: str, T: T };
|
|
615
333
|
}
|
|
616
334
|
|
|
617
|
-
/* Logo block for a banner/panel header.
|
|
618
|
-
|
|
619
|
-
Dark theme: branding.logoDark, when supplied, is rendered as a second node
|
|
620
|
-
and swapped purely in CSS. When it is absent the single main logo shows in
|
|
621
|
-
both themes — which is why the shipped ECOM Consult asset (wordmark
|
|
622
|
-
fill="white", built for dark backgrounds) belongs in logoDark, with a
|
|
623
|
-
dark-ink variant in logo. An <img>/data: logo cannot be recoloured by our
|
|
624
|
-
CSS at all, so two assets are the only route there; an inline SVG could in
|
|
625
|
-
principle inherit currentColor, but only if the asset is authored that way. */
|
|
626
335
|
function buildBrandLogo(cfg) {
|
|
627
|
-
var b =
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
var alt = str(b.logoAlt) || '';
|
|
631
|
-
var main = buildLogoNode(b.logo, alt, false);
|
|
632
|
-
if (!main) return null; // no valid logo -> render nothing
|
|
633
|
-
|
|
634
|
-
var dark = buildLogoNode(b.logoDark, alt, false);
|
|
635
|
-
|
|
636
|
-
var wrap = el('div', 'ck-brand');
|
|
637
|
-
if (dark) {
|
|
638
|
-
wrap.classList.add('ck-brand__has-dark');
|
|
639
|
-
main.classList.add('ck-brand__light');
|
|
640
|
-
dark.classList.add('ck-brand__dark');
|
|
641
|
-
}
|
|
336
|
+
var b = brandingExt();
|
|
337
|
+
return (b && typeof b.buildBrandLogo === 'function') ? b.buildBrandLogo(cfg, extHost()) : null;
|
|
338
|
+
}
|
|
642
339
|
|
|
643
|
-
|
|
644
|
-
var
|
|
645
|
-
|
|
646
|
-
var a = el('a', 'ck-brand__link');
|
|
647
|
-
a.href = link;
|
|
648
|
-
a.target = '_blank';
|
|
649
|
-
a.rel = 'noopener noreferrer';
|
|
650
|
-
// Links are focusable by nature; the accessible name comes from logoAlt.
|
|
651
|
-
a.setAttribute('aria-label', alt || 'ConsentKit');
|
|
652
|
-
host_ = a;
|
|
653
|
-
wrap.appendChild(a);
|
|
654
|
-
}
|
|
655
|
-
host_.appendChild(main);
|
|
656
|
-
if (dark) host_.appendChild(dark);
|
|
657
|
-
|
|
658
|
-
// A non-linked logo must not be a tab stop. The <svg>/<img> is aria-hidden
|
|
659
|
-
// or alt="", so a visually-hidden-free text alternative is supplied here
|
|
660
|
-
// for the image case only when it is not already announced by the <img> alt.
|
|
661
|
-
if (!link && alt && main.nodeName.toLowerCase() === 'svg') {
|
|
662
|
-
wrap.setAttribute('role', 'img');
|
|
663
|
-
wrap.setAttribute('aria-label', alt);
|
|
664
|
-
}
|
|
665
|
-
return wrap;
|
|
340
|
+
function buildPoweredBy(cfg) {
|
|
341
|
+
var b = brandingExt();
|
|
342
|
+
return (b && typeof b.buildPoweredBy === 'function') ? b.buildPoweredBy(cfg, extHost()) : null;
|
|
666
343
|
}
|
|
667
344
|
|
|
668
|
-
// Per-mount CSS for logo height + the dark/light swap. Mirrors buildThemeCss.
|
|
669
345
|
function buildBrandCss(cfg) {
|
|
670
|
-
var b =
|
|
671
|
-
|
|
672
|
-
var h = clampLogoHeight(b.logoHeight);
|
|
673
|
-
var out = [':host{--ck-logo-h:' + h + 'px}'];
|
|
674
|
-
|
|
675
|
-
var theme = (cfg && cfg.theme) || {};
|
|
676
|
-
var mode = theme.mode;
|
|
677
|
-
if (mode !== 'light' && mode !== 'dark') mode = 'auto';
|
|
678
|
-
|
|
679
|
-
// Same cascade shape as buildThemeCss so the logo always agrees with the
|
|
680
|
-
// palette: auto mode follows prefers-color-scheme but a forced .ck-mode-light
|
|
681
|
-
// still wins, and .ck-mode-dark forces the dark asset outright.
|
|
682
|
-
function swap(prefix) {
|
|
683
|
-
return prefix + ' .ck-brand__has-dark .ck-brand__light{display:none}\n' +
|
|
684
|
-
prefix + ' .ck-brand__has-dark .ck-brand__dark{display:block}';
|
|
685
|
-
}
|
|
686
|
-
if (mode === 'auto') {
|
|
687
|
-
out.push('@media (prefers-color-scheme: dark){\n' +
|
|
688
|
-
swap(':host(:not(.ck-mode-light))') + '\n}');
|
|
689
|
-
}
|
|
690
|
-
out.push(swap(':host(.ck-mode-dark)'));
|
|
691
|
-
return out.join('\n');
|
|
346
|
+
var b = brandingExt();
|
|
347
|
+
return (b && typeof b.buildBrandCss === 'function') ? (b.buildBrandCss(cfg) || '') : '';
|
|
692
348
|
}
|
|
693
349
|
|
|
694
|
-
/*
|
|
695
|
-
|
|
696
|
-
function
|
|
697
|
-
var b =
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
if (!pb) return null; // false/undefined -> nothing
|
|
701
|
-
|
|
702
|
-
var text, url = null;
|
|
703
|
-
if (pb === true) {
|
|
704
|
-
text = T.poweredBy; // en fallback guaranteed by STR_KEYS
|
|
705
|
-
} else if (typeof pb === 'object' && !Array.isArray(pb)) {
|
|
706
|
-
text = str(pb.text) || T.poweredBy;
|
|
707
|
-
url = safeLinkUrl(pb.url);
|
|
708
|
-
} else {
|
|
709
|
-
return null;
|
|
710
|
-
}
|
|
350
|
+
/* Static branding rules, appended to the base sheet at mount. Empty string
|
|
351
|
+
when the extension is absent — no dead .ck-brand CSS in the build. */
|
|
352
|
+
function brandingCss() {
|
|
353
|
+
var b = brandingExt();
|
|
354
|
+
return (b && typeof b.css === 'function') ? ('\n' + b.css()) : '';
|
|
355
|
+
}
|
|
711
356
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
p.appendChild(a);
|
|
719
|
-
} else {
|
|
720
|
-
p.appendChild(document.createTextNode(text));
|
|
721
|
-
}
|
|
722
|
-
return p;
|
|
357
|
+
/* Structural: branding produces DOM, so a config that gains a logo must
|
|
358
|
+
remount. '-' whenever there is no branding at all, which keeps an
|
|
359
|
+
unbranded page's signature identical to what it was before the split. */
|
|
360
|
+
function brandSignature(cfg) {
|
|
361
|
+
var b = brandingExt();
|
|
362
|
+
return (b && typeof b.brandSignature === 'function') ? b.brandSignature(cfg) : '-';
|
|
723
363
|
}
|
|
724
364
|
|
|
725
365
|
function safeState() {
|
|
@@ -1289,25 +929,9 @@
|
|
|
1289
929
|
// Structural inputs: a change to any of these needs a rebuild, not a restyle.
|
|
1290
930
|
// Branding belongs here because it produces DOM, not just styling: mount() is
|
|
1291
931
|
// one-shot, so a config that gains a logo after the first ck:init would
|
|
1292
|
-
// otherwise take the applyTheme()-only path and never render it.
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
if (!b) return '-';
|
|
1296
|
-
var pb = b.poweredBy;
|
|
1297
|
-
var pbSig = (pb && typeof pb === 'object')
|
|
1298
|
-
? 'o:' + String(pb.text || '') + ':' + String(pb.url || '')
|
|
1299
|
-
: String(!!pb);
|
|
1300
|
-
// Logos are hashed by length + head so a long data: URI does not bloat the key.
|
|
1301
|
-
function tag(v) {
|
|
1302
|
-
var s = str(v);
|
|
1303
|
-
return s ? (s.length + ':' + s.slice(0, 32)) : '-';
|
|
1304
|
-
}
|
|
1305
|
-
return [
|
|
1306
|
-
tag(b.logo), tag(b.logoDark), String(b.logoAlt || ''),
|
|
1307
|
-
String(clampLogoHeight(b.logoHeight)), String(b.logoUrl || ''), pbSig
|
|
1308
|
-
].join('~');
|
|
1309
|
-
}
|
|
1310
|
-
|
|
932
|
+
// otherwise take the applyTheme()-only path and never render it. The signature
|
|
933
|
+
// itself is computed by the branding extension (and is '-' without it), so an
|
|
934
|
+
// unbranded page's signature is byte-identical to what it was before the split.
|
|
1311
935
|
function signature(cfg) {
|
|
1312
936
|
var c = cfg || {};
|
|
1313
937
|
var lay = resolveLayout(c);
|
|
@@ -1347,7 +971,10 @@
|
|
|
1347
971
|
root.innerHTML = '';
|
|
1348
972
|
|
|
1349
973
|
var style = document.createElement('style');
|
|
1350
|
-
|
|
974
|
+
// Base sheet plus the branding rules, when the branding file is loaded.
|
|
975
|
+
// Without it brandingCss() is '' and the shadow root carries no .ck-brand,
|
|
976
|
+
// .ck-foot or .ck-powered rules at all.
|
|
977
|
+
style.textContent = CSS + brandingCss();
|
|
1351
978
|
root.appendChild(style);
|
|
1352
979
|
|
|
1353
980
|
switches = {};
|