@ecomconsult/consentkit 0.4.0 → 0.5.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.
- package/README.md +117 -19
- package/npm/index.d.ts +160 -2
- package/npm/internal-stub.mjs +6 -1
- package/package.json +1 -1
- package/src/ck-core.js +195 -14
- package/src/ck-debug.js +120 -1
- package/src/ck-ui.js +489 -43
package/src/ck-core.js
CHANGED
|
@@ -77,6 +77,10 @@
|
|
|
77
77
|
'nr-data.net': 'analytics', // New Relic beacon
|
|
78
78
|
'datadoghq.com': 'analytics', // RUM
|
|
79
79
|
'datadoghq-browser-agent.com': 'analytics',
|
|
80
|
+
// Cloudflare Web Analytics. Subdomain only, and deliberately NOT in
|
|
81
|
+
// INFRA_DB: the rest of Cloudflare's edge is infrastructure, but this one
|
|
82
|
+
// beacon is a measurement product (§8 names it by hand for that reason).
|
|
83
|
+
'static.cloudflareinsights.com': 'analytics',
|
|
80
84
|
|
|
81
85
|
// --- marketing -----------------------------------------------------
|
|
82
86
|
'connect.facebook.net': 'marketing',
|
|
@@ -119,7 +123,27 @@
|
|
|
119
123
|
'jivosite.com': 'functional',
|
|
120
124
|
'jivo.chat': 'functional',
|
|
121
125
|
'tidio.co': 'functional',
|
|
122
|
-
'tidiochat.com': 'functional'
|
|
126
|
+
'tidiochat.com': 'functional',
|
|
127
|
+
// Searchanise — site search for e-commerce. The widget IS the search box,
|
|
128
|
+
// so a shop whose visitor declines functional loses the feature and nothing
|
|
129
|
+
// else; it is not measurement. The numbered API hosts are enumerated
|
|
130
|
+
// because matching here is plain suffix matching (see hostMatches) with no
|
|
131
|
+
// pattern form: searchserverapi1.com is not a subdomain of anything.
|
|
132
|
+
'searchanise.com': 'functional',
|
|
133
|
+
'searchserverapi.com': 'functional',
|
|
134
|
+
'searchserverapi1.com': 'functional',
|
|
135
|
+
'searchserverapi2.com': 'functional',
|
|
136
|
+
'searchserverapi3.com': 'functional',
|
|
137
|
+
// iuteCredit — a Moldovan consumer-credit calculator embedded on shop
|
|
138
|
+
// product pages. A third party the owner chose, offering a feature.
|
|
139
|
+
'iutecredit.md': 'functional',
|
|
140
|
+
|
|
141
|
+
// --- necessary -----------------------------------------------------
|
|
142
|
+
// recaptcha.net is Google's alternate reCAPTCHA domain, served for regions
|
|
143
|
+
// where google.com is unreachable. Unlike the two path-scoped Google hosts
|
|
144
|
+
// in PATH_DB, this domain hosts NOTHING but the captcha, so the whole host
|
|
145
|
+
// is the right scope.
|
|
146
|
+
'recaptcha.net': 'necessary'
|
|
123
147
|
};
|
|
124
148
|
|
|
125
149
|
// Runtime overrides fed in by ConsentKit._extendHostDb(map) — the SaaS
|
|
@@ -145,8 +169,20 @@
|
|
|
145
169
|
// tags never get the chance to see consent at all.
|
|
146
170
|
// gtag.js is the opposite case — a direct GA4/Ads install with no container
|
|
147
171
|
// in front of it — so it stays blocked by path.
|
|
172
|
+
//
|
|
173
|
+
// reCAPTCHA is the other reason this table exists. www.google.com and
|
|
174
|
+
// www.gstatic.com host it, and neither can be given a category as a WHOLE
|
|
175
|
+
// host — that would rule on every Google property at once. Path-scoping names
|
|
176
|
+
// the widget and nothing else. The category is 'necessary': a form gated
|
|
177
|
+
// behind a captcha is unusable without it, and allowed() lets 'necessary'
|
|
178
|
+
// through unconditionally, so this classifies the request for the report
|
|
179
|
+
// without ever blocking it. (Strict mode already passed these through via
|
|
180
|
+
// BASE_ALLOW_PATH; what this adds is a NAME, so the audit stops filing the
|
|
181
|
+
// captcha under «сторонние подключения без категории».)
|
|
148
182
|
var PATH_DB = {
|
|
149
183
|
'/gtag/js': 'analytics', // googletagmanager.com/gtag/js?id=G-XXXX
|
|
184
|
+
'www.google.com/recaptcha/': 'necessary',
|
|
185
|
+
'www.gstatic.com/recaptcha/': 'necessary',
|
|
150
186
|
'/trackers/ga.js': 'analytics',
|
|
151
187
|
'/trackers/pixel.js': 'marketing',
|
|
152
188
|
'/trackers/chat.js': 'functional'
|
|
@@ -215,7 +251,16 @@
|
|
|
215
251
|
policyVersion: '1',
|
|
216
252
|
language: 'auto',
|
|
217
253
|
layout: { type: 'bar', position: 'bottom' },
|
|
218
|
-
|
|
254
|
+
// 0.5.0: radius/font/buttons are resolved in ck-ui.js, not defaulted here.
|
|
255
|
+
// A concrete default in this object would be merged into every config and
|
|
256
|
+
// would then shadow the UI's own defaults — which is exactly what happened
|
|
257
|
+
// to `radius: '10px'` before 0.5.0: it pinned the pre-reference geometry on
|
|
258
|
+
// every site whether or not the owner had ever set it.
|
|
259
|
+
theme: { accent: '#2B50D8' },
|
|
260
|
+
// 0.5.0 (SPEC V1.6 §2). `detailsAction` is deliberately absent rather than
|
|
261
|
+
// set: its default depends on whether policyUrl is present, and a merged
|
|
262
|
+
// concrete value here would make that URL-sensitive default unreachable.
|
|
263
|
+
texts: {},
|
|
219
264
|
categories: {
|
|
220
265
|
functional: { enabled: true },
|
|
221
266
|
analytics: { enabled: true },
|
|
@@ -231,22 +276,82 @@
|
|
|
231
276
|
cookieTable: []
|
|
232
277
|
};
|
|
233
278
|
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
|
|
279
|
+
// Infrastructure (§8) — NOT a consent category, a CLASS of host.
|
|
280
|
+
//
|
|
281
|
+
// These are the CDNs, static hosts and font services that site builders and
|
|
282
|
+
// hosting platforms serve a site's OWN markup, styles and scripts from. A
|
|
283
|
+
// Tilda page loads its own layout from tildacdn.com; a Wix page loads its own
|
|
284
|
+
// from parastorage.com. Reporting those to the site owner as «сторонние
|
|
285
|
+
// подключения без категории» is noise about something they cannot decide:
|
|
286
|
+
// there is no consent question to answer and no switch to flip. §8 exists to
|
|
287
|
+
// stop the audit spending the owner's attention on them.
|
|
288
|
+
//
|
|
289
|
+
// Two consequences, both of them the point:
|
|
290
|
+
// - strict mode NEVER intercepts these (a builder's dynamic modules break
|
|
291
|
+
// without their own CDN, and blocking them breaks the site, not a tracker);
|
|
292
|
+
// - the scanner flags them `infra: true`, keeps them out of the summary and
|
|
293
|
+
// never writes them to tracker_observations.
|
|
294
|
+
//
|
|
295
|
+
// Suffix-matched, exactly like HOST_DB. Membership here is not a claim that a
|
|
296
|
+
// host is harmless in general — it is a claim that it serves the site's own
|
|
297
|
+
// assets. Anything that MEASURES belongs in HOST_DB with a real category
|
|
298
|
+
// instead: static.cloudflareinsights.com is the worked example (§8 calls it
|
|
299
|
+
// out by name), and it sits above as `analytics` for that reason. Because
|
|
300
|
+
// categoryForUrl is consulted before the strict allowlist, a host in both
|
|
301
|
+
// places is classified, not waved through — so a bare `cloudflare.com` here
|
|
302
|
+
// would be a bug.
|
|
303
|
+
var INFRA_DB = [
|
|
304
|
+
// --- site builders and hosting platforms ---------------------------
|
|
305
|
+
'tildacdn.com',
|
|
306
|
+
'tildacdn.net',
|
|
307
|
+
'tilda.ws',
|
|
308
|
+
'static.wixstatic.com',
|
|
309
|
+
'parastorage.com', // Wix static assets
|
|
310
|
+
'cdn.shopify.com',
|
|
311
|
+
'squarespace-cdn.com',
|
|
312
|
+
'assets.website-files.com', // Webflow
|
|
313
|
+
// --- general asset CDNs --------------------------------------------
|
|
243
314
|
'cdn.jsdelivr.net',
|
|
244
315
|
'unpkg.com',
|
|
245
316
|
'cdnjs.cloudflare.com',
|
|
246
317
|
'code.jquery.com',
|
|
318
|
+
'ajax.googleapis.com',
|
|
319
|
+
'ajax.aspnetcdn.com', // Microsoft Ajax CDN (jQuery, MVC)
|
|
320
|
+
'aspnetcdn.com',
|
|
321
|
+
'kxcdn.com', // KeyCDN tenants, e.g. *-ef84.kxcdn.com
|
|
322
|
+
'cloudfront.net', // AWS CloudFront distributions
|
|
323
|
+
'akamaihd.net', // Akamai
|
|
324
|
+
'fastly.net',
|
|
325
|
+
'b-cdn.net', // bunny.net
|
|
326
|
+
// --- fonts ----------------------------------------------------------
|
|
247
327
|
'fonts.googleapis.com',
|
|
328
|
+
// gstatic.com whole: Google's static-asset domain. Its subdomains serve
|
|
329
|
+
// fonts (fonts.gstatic.com), the reCAPTCHA widget's own code
|
|
330
|
+
// (www.gstatic.com/recaptcha/) and ordinary images — assets a page is
|
|
331
|
+
// broken without, none of which measure a visitor. Nothing on it is a
|
|
332
|
+
// tracker: the Google properties that DO measure live on their own hosts
|
|
333
|
+
// (google-analytics.com, googletagmanager.com, doubleclick.net) and are
|
|
334
|
+
// classified there. fonts.gstatic.com is kept below it, redundant under
|
|
335
|
+
// suffix matching but named because §8 and the fixtures both refer to it.
|
|
336
|
+
'gstatic.com',
|
|
248
337
|
'fonts.gstatic.com',
|
|
338
|
+
// --- captcha ---------------------------------------------------------
|
|
339
|
+
// A page whose form is gated behind a captcha is unusable without it. The
|
|
340
|
+
// Google-hosted halves are path-scoped in BASE_ALLOW_PATH below, since
|
|
341
|
+
// www.google.com and www.gstatic.com cannot be waved through wholesale.
|
|
249
342
|
'hcaptcha.com',
|
|
343
|
+
// --- our own service -------------------------------------------------
|
|
344
|
+
// The consent tool must not report itself as an unnamed third party.
|
|
345
|
+
'consent.ecomconsult.net'
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
// Hosts strict mode never intercepts that are NOT infrastructure: things a
|
|
349
|
+
// page is broken or unusable without, but which are somebody's product
|
|
350
|
+
// rather than the site's own asset delivery. Kept separate from INFRA_DB so
|
|
351
|
+
// that «инфраструктура» in a report means what §8 says it means — a payment
|
|
352
|
+
// form is a third party the owner chose, and calling it infrastructure would
|
|
353
|
+
// hide a real decision. Both lists feed strict mode identically.
|
|
354
|
+
var BASE_ALLOW = [
|
|
250
355
|
'js.stripe.com',
|
|
251
356
|
'pay.google.com',
|
|
252
357
|
'checkout.creem.io'
|
|
@@ -254,6 +359,12 @@
|
|
|
254
359
|
|
|
255
360
|
// Path-scoped members of the base allowlist: allowed only on this exact path
|
|
256
361
|
// prefix, because the host at large is not something to wave through.
|
|
362
|
+
//
|
|
363
|
+
// §8 lists these two under infrastructure, and they are — but they cannot
|
|
364
|
+
// live in INFRA_DB, which is a flat list of suffix-matched HOSTS. Allowing
|
|
365
|
+
// `www.google.com` as a host would wave through every Google property, and
|
|
366
|
+
// `_isInfra(host)` has no path to test. So they stay here: strict mode treats
|
|
367
|
+
// them exactly as it treats INFRA_DB, and `_infra()` reports hosts only.
|
|
257
368
|
var BASE_ALLOW_PATH = [
|
|
258
369
|
{ host: 'www.google.com', path: '/recaptcha' },
|
|
259
370
|
{ host: 'www.gstatic.com', path: '/recaptcha' }
|
|
@@ -760,7 +871,32 @@
|
|
|
760
871
|
return !!r && r === pr;
|
|
761
872
|
}
|
|
762
873
|
|
|
874
|
+
// Is this host infrastructure (§8)? Suffix-matched, like everything else.
|
|
875
|
+
//
|
|
876
|
+
// `ConsentKitDebugUrl` is resolved HERE rather than being baked into
|
|
877
|
+
// INFRA_DB, because it is a runtime window global a site sets to point the
|
|
878
|
+
// debug panel at its own mirror. Reading it at call time is the only way it
|
|
879
|
+
// can be covered at all; a snapshot taken when this file was evaluated would
|
|
880
|
+
// miss every page that sets it after the core loads.
|
|
881
|
+
function isInfraHost(host) {
|
|
882
|
+
if (!host) { return false; }
|
|
883
|
+
for (var i = 0; i < INFRA_DB.length; i++) {
|
|
884
|
+
if (hostMatches(host, INFRA_DB[i])) { return true; }
|
|
885
|
+
}
|
|
886
|
+
try {
|
|
887
|
+
var dbg = global.ConsentKitDebugUrl;
|
|
888
|
+
if (typeof dbg === 'string' && dbg) {
|
|
889
|
+
var dh = urlParts(dbg).host;
|
|
890
|
+
if (dh && dh === host) { return true; }
|
|
891
|
+
}
|
|
892
|
+
} catch (e) { /* noop */ }
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
|
|
763
896
|
function baseAllowed(host, url) {
|
|
897
|
+
// §8: infrastructure is never intercepted by strict mode. Checked first —
|
|
898
|
+
// it is the larger list and the commoner case on a builder-hosted site.
|
|
899
|
+
if (isInfraHost(host)) { return true; }
|
|
764
900
|
for (var i = 0; i < BASE_ALLOW.length; i++) {
|
|
765
901
|
if (hostMatches(host, BASE_ALLOW[i])) { return true; }
|
|
766
902
|
}
|
|
@@ -1325,7 +1461,7 @@
|
|
|
1325
1461
|
// Public API
|
|
1326
1462
|
// ---------------------------------------------------------------------------
|
|
1327
1463
|
var ConsentKit = {
|
|
1328
|
-
version: '0.
|
|
1464
|
+
version: '0.5.1',
|
|
1329
1465
|
config: config,
|
|
1330
1466
|
|
|
1331
1467
|
init: function (userConfig) {
|
|
@@ -1400,7 +1536,11 @@
|
|
|
1400
1536
|
},
|
|
1401
1537
|
|
|
1402
1538
|
// The built-in strict-mode allowlist, exported for docs and tests so the
|
|
1403
|
-
// list a site owner reads is the list the engine actually uses.
|
|
1539
|
+
// list a site owner reads is the list the engine actually uses. Since 0.4.1
|
|
1540
|
+
// that is INFRA_DB (§8) plus the payment/captcha set plus the path-scoped
|
|
1541
|
+
// entries — everything baseAllowed() consults, in one list, because what a
|
|
1542
|
+
// site owner needs to know is «what does strict mode let through», not how
|
|
1543
|
+
// the engine files it internally.
|
|
1404
1544
|
//
|
|
1405
1545
|
// A GETTER, not a plain array: a plain property is evaluated once, and the
|
|
1406
1546
|
// single array it produced would be handed to every caller — one
|
|
@@ -1408,7 +1548,48 @@
|
|
|
1408
1548
|
// widen what strict mode lets through for the rest of the page load.
|
|
1409
1549
|
// Each read returns a fresh copy, so the list is readable and inert.
|
|
1410
1550
|
get _baseAllow() {
|
|
1411
|
-
return
|
|
1551
|
+
return INFRA_DB.slice()
|
|
1552
|
+
.concat(BASE_ALLOW)
|
|
1553
|
+
.concat(BASE_ALLOW_PATH.map(function (e) { return e.host + e.path; }));
|
|
1554
|
+
},
|
|
1555
|
+
|
|
1556
|
+
// The infrastructure list (§8): CDNs and static hosts of site builders and
|
|
1557
|
+
// hosting platforms, general asset CDNs, fonts and captcha. A CLASS of
|
|
1558
|
+
// host, not a consent category — nothing here is a decision the site owner
|
|
1559
|
+
// gets to make, which is exactly why the audit stops reporting them.
|
|
1560
|
+
//
|
|
1561
|
+
// Shared with the SaaS scanner, which loads this core and reads this list
|
|
1562
|
+
// to set `thirdParty[].infra`, keep infrastructure out of the summary and
|
|
1563
|
+
// out of tracker_observations. Hosts only: the two path-scoped recaptcha
|
|
1564
|
+
// entries are infrastructure too, but cannot be expressed as bare hosts —
|
|
1565
|
+
// see BASE_ALLOW_PATH. A fresh copy per read, for the same reason as
|
|
1566
|
+
// _baseAllow.
|
|
1567
|
+
_infra: function () {
|
|
1568
|
+
return INFRA_DB.slice();
|
|
1569
|
+
},
|
|
1570
|
+
|
|
1571
|
+
// Is this URL infrastructure? Takes a URL, like every other public
|
|
1572
|
+
// predicate here, so a caller never has to reimplement host extraction.
|
|
1573
|
+
//
|
|
1574
|
+
// A BARE HOST is accepted too ('tildacdn.com'), because the scanner works
|
|
1575
|
+
// in hosts and would otherwise have to glue a fake scheme on every call.
|
|
1576
|
+
// It needs its own branch: urlParts resolves a relative string against the
|
|
1577
|
+
// page, so a bare host arrives as a PATH on the first-party origin and
|
|
1578
|
+
// would answer false. The test is deliberately narrow — a string with no
|
|
1579
|
+
// scheme, no slash, no query and at least one dot is a hostname and cannot
|
|
1580
|
+
// be anything else.
|
|
1581
|
+
_isInfra: function (url) {
|
|
1582
|
+
try {
|
|
1583
|
+
if (!url || typeof url !== 'string') { return false; }
|
|
1584
|
+
var s = url.trim();
|
|
1585
|
+
if (!s) { return false; }
|
|
1586
|
+
if (/^[a-z0-9.\-]+\.[a-z0-9\-]+$/i.test(s)) {
|
|
1587
|
+
return isInfraHost(s.toLowerCase().replace(/^\.+|\.+$/g, ''));
|
|
1588
|
+
}
|
|
1589
|
+
var host = urlParts(s).host;
|
|
1590
|
+
if (!host) { return false; }
|
|
1591
|
+
return isInfraHost(host);
|
|
1592
|
+
} catch (e) { return false; }
|
|
1412
1593
|
},
|
|
1413
1594
|
|
|
1414
1595
|
// What is being held back until consent: everything the engine intercepted
|
package/src/ck-debug.js
CHANGED
|
@@ -199,6 +199,28 @@
|
|
|
199
199
|
note: NOTE_RU,
|
|
200
200
|
secConsentMode: 'Consent Mode / dataLayer',
|
|
201
201
|
noEvents: 'событий не было',
|
|
202
|
+
secTheme: 'Оформление',
|
|
203
|
+
themeMode: 'тема',
|
|
204
|
+
themeModeLight: 'светлая',
|
|
205
|
+
themeModeDark: 'тёмная',
|
|
206
|
+
themeFont: 'шрифт',
|
|
207
|
+
themeFontInherit: 'как на сайте',
|
|
208
|
+
themeFontSystem: 'системный',
|
|
209
|
+
themeRadius: 'скругления',
|
|
210
|
+
themeCard: 'карточка',
|
|
211
|
+
themeBtn: 'кнопки',
|
|
212
|
+
themeLink: 'Ссылки',
|
|
213
|
+
btnAccept: 'Принять всё',
|
|
214
|
+
btnReject: 'Отклонить всё',
|
|
215
|
+
btnSettings: 'Настроить',
|
|
216
|
+
btnFilled: 'залитая',
|
|
217
|
+
btnOutline: 'обводка',
|
|
218
|
+
btnText: 'текст',
|
|
219
|
+
btnBorder: 'обводка',
|
|
220
|
+
btnOn: 'на',
|
|
221
|
+
btnAdjusted: 'исправлено автоматически',
|
|
222
|
+
btnOk: 'AA',
|
|
223
|
+
btnFail: 'ниже AA',
|
|
202
224
|
secActions: 'Действия',
|
|
203
225
|
reset: 'Сбросить согласие',
|
|
204
226
|
showPrefs: 'Показать настройки',
|
|
@@ -236,6 +258,28 @@
|
|
|
236
258
|
note: NOTE_EN,
|
|
237
259
|
secConsentMode: 'Consent Mode / dataLayer',
|
|
238
260
|
noEvents: 'no events',
|
|
261
|
+
secTheme: 'Appearance',
|
|
262
|
+
themeMode: 'theme',
|
|
263
|
+
themeModeLight: 'light',
|
|
264
|
+
themeModeDark: 'dark',
|
|
265
|
+
themeFont: 'font',
|
|
266
|
+
themeFontInherit: 'site font',
|
|
267
|
+
themeFontSystem: 'system',
|
|
268
|
+
themeRadius: 'radii',
|
|
269
|
+
themeCard: 'card',
|
|
270
|
+
themeBtn: 'buttons',
|
|
271
|
+
themeLink: 'Links',
|
|
272
|
+
btnAccept: 'Accept all',
|
|
273
|
+
btnReject: 'Reject all',
|
|
274
|
+
btnSettings: 'Customize',
|
|
275
|
+
btnFilled: 'filled',
|
|
276
|
+
btnOutline: 'outline',
|
|
277
|
+
btnText: 'text',
|
|
278
|
+
btnBorder: 'border',
|
|
279
|
+
btnOn: 'on',
|
|
280
|
+
btnAdjusted: 'adjusted automatically',
|
|
281
|
+
btnOk: 'AA',
|
|
282
|
+
btnFail: 'below AA',
|
|
239
283
|
secActions: 'Actions',
|
|
240
284
|
reset: 'Reset consent',
|
|
241
285
|
showPrefs: 'Show preferences',
|
|
@@ -547,6 +591,13 @@
|
|
|
547
591
|
|
|
548
592
|
function tag(text, cls) { return el('span', { class: 't' + (cls ? ' ' + cls : '') }, text); }
|
|
549
593
|
|
|
594
|
+
// A contrast ratio the way WCAG tools quote it. null when the colour could
|
|
595
|
+
// not be measured (a colour name, an rgb() string) — shown as "?" rather
|
|
596
|
+
// than a made-up number.
|
|
597
|
+
function fmtRatio(r) {
|
|
598
|
+
return typeof r === 'number' && isFinite(r) ? (Math.round(r * 100) / 100) + ':1' : '?';
|
|
599
|
+
}
|
|
600
|
+
|
|
550
601
|
// Resolved lazily, not at parse time: this file runs before ConsentKit.init()
|
|
551
602
|
// has merged the site's config, so asking for the language now would always
|
|
552
603
|
// read the built-in default. Re-resolved on every render so a page that
|
|
@@ -663,7 +714,75 @@
|
|
|
663
714
|
}
|
|
664
715
|
body.appendChild(s5);
|
|
665
716
|
|
|
666
|
-
// 6.
|
|
717
|
+
// 6. Appearance (SPEC V1.6 §1) — the three buttons' RESOLVED colours and
|
|
718
|
+
// their contrast ratios, read straight off ConsentKit._contrast rather than
|
|
719
|
+
// recomputed here: the banner, this panel and the cabinet's theme editor
|
|
720
|
+
// must all quote the same numbers. The mode is named explicitly because
|
|
721
|
+
// light and dark resolve differently and only one of them is on screen.
|
|
722
|
+
var contrast = CK && CK._contrast;
|
|
723
|
+
if (contrast && typeof contrast.buildThemeCss === 'function') {
|
|
724
|
+
var sT = section(T.secTheme);
|
|
725
|
+
try {
|
|
726
|
+
var built = contrast.buildThemeCss((CK && CK.config) || {});
|
|
727
|
+
// Which palette the visitor is actually looking at right now.
|
|
728
|
+
var dark = built.mode === 'dark';
|
|
729
|
+
if (built.mode === 'auto') {
|
|
730
|
+
try {
|
|
731
|
+
dark = !!(global.matchMedia && global.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
732
|
+
} catch (e) { dark = false; }
|
|
733
|
+
}
|
|
734
|
+
var resolved = dark ? built.dark : built.light;
|
|
735
|
+
|
|
736
|
+
// Fourth row: --ck-link, the accent as TEXT on the card. It is a
|
|
737
|
+
// separate number from every button's, because it is measured against
|
|
738
|
+
// the card rather than against a button fill — a brand accent can pass
|
|
739
|
+
// as a filled button's background and still be unreadable as a link.
|
|
740
|
+
var lk = resolved.link;
|
|
741
|
+
var lkTxt = '—';
|
|
742
|
+
if (lk) {
|
|
743
|
+
lkTxt = lk.color + ' ' + T.btnOn + ' ' + lk.against +
|
|
744
|
+
' · ' + fmtRatio(lk.ratio) +
|
|
745
|
+
' ' + ((typeof lk.ratio === 'number' && lk.ratio >= 4.5) ? T.btnOk : T.btnFail) +
|
|
746
|
+
(lk.adjusted ? ' · ' + T.btnAdjusted : '');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
sT.appendChild(defs([
|
|
750
|
+
[T.themeMode, (dark ? T.themeModeDark : T.themeModeLight) +
|
|
751
|
+
(built.mode === 'auto' ? ' (auto)' : '')],
|
|
752
|
+
[T.themeFont, built.font === 'inherit' ? T.themeFontInherit : T.themeFontSystem],
|
|
753
|
+
[T.themeRadius, T.themeCard + ' ' + built.radius.card + 'px · ' +
|
|
754
|
+
T.themeBtn + ' ' + built.radius.button + 'px'],
|
|
755
|
+
[T.themeLink, lkTxt]
|
|
756
|
+
]));
|
|
757
|
+
|
|
758
|
+
var labels = { accept: T.btnAccept, reject: T.btnReject, settings: T.btnSettings };
|
|
759
|
+
var uT = doc.createElement('ul');
|
|
760
|
+
['accept', 'reject', 'settings'].forEach(function (role) {
|
|
761
|
+
var b = resolved.buttons[role];
|
|
762
|
+
if (!b) { return; }
|
|
763
|
+
var li = doc.createElement('li');
|
|
764
|
+
li.appendChild(tag(b.variant === 'filled' ? T.btnFilled : T.btnOutline));
|
|
765
|
+
var ok = typeof b.ratio === 'number' && b.ratio >= 4.5;
|
|
766
|
+
li.appendChild(tag(fmtRatio(b.ratio) + ' ' + (ok ? T.btnOk : T.btnFail),
|
|
767
|
+
ok ? 'on' : 'off'));
|
|
768
|
+
li.appendChild(doc.createTextNode(labels[role] + ' — ' +
|
|
769
|
+
T.btnText + ' ' + b.fg + ' ' + T.btnOn + ' ' + b.against +
|
|
770
|
+
(b.variant === 'outline'
|
|
771
|
+
? ' · ' + T.btnBorder + ' ' + b.border + ' (' + fmtRatio(b.borderRatio) + ')'
|
|
772
|
+
: '')));
|
|
773
|
+
if (b.adjusted) {
|
|
774
|
+
li.appendChild(el('span', { class: 'mut' }, ' · ' + T.btnAdjusted));
|
|
775
|
+
}
|
|
776
|
+
uT.appendChild(li);
|
|
777
|
+
});
|
|
778
|
+
sT.appendChild(uT);
|
|
779
|
+
} catch (e) {
|
|
780
|
+
sT.appendChild(el('div', { class: 'mut' }, '—'));
|
|
781
|
+
}
|
|
782
|
+
body.appendChild(sT);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// 7. Buttons
|
|
667
786
|
var s6 = section(T.secActions);
|
|
668
787
|
var row = el('div', { class: 'row' });
|
|
669
788
|
var bReset = el('button', { type: 'button' }, T.reset);
|