@ecomconsult/consentkit 0.5.8 → 0.5.10
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 +26 -14
- package/npm/internal-stub.mjs +1 -1
- package/package.json +1 -1
- package/src/ck-core.js +87 -3
- package/src/ck-debug.js +34 -5
- package/src/ck-ui.js +146 -29
package/README.md
CHANGED
|
@@ -184,9 +184,10 @@ Pass any subset to `init()`. Nested objects merge with the defaults.
|
|
|
184
184
|
| `theme.accent` | `string` | `"#2B50D8"` | Exposed as `--ck-accent` |
|
|
185
185
|
| `theme.font` | `"inherit" \| "system"` | `"inherit"` | v0.5.0. `inherit` takes the host page's font family; `system` restores the pre-0.5.0 system stack. Font *sizes* are fixed either way |
|
|
186
186
|
| `theme.radius` | `{ card, button }` | `{ card: 16, button: 8 }` | v0.5.0. px, clamped 0–32. A bare string or number is the pre-0.5.0 form and still sets the card radius |
|
|
187
|
-
| `theme.buttons` | `{ accept, reject, settings }` | see below | v0.5.0. Per-button appearance.
|
|
187
|
+
| `theme.buttons` | `{ accept, reject, settings }` | see below | v0.5.0. Per-button appearance. A colour you set is painted as set; contrast rules correct only derived colours (v0.5.10) — see [Button appearance](#button-appearance) |
|
|
188
188
|
| `theme.mode` | `"auto" \| "light" \| "dark"` | `"auto"` | `auto` follows `prefers-color-scheme` |
|
|
189
189
|
| `theme.dark` | `{ bg, ink, accent, onAccent }` | built-in | Overrides the dark palette |
|
|
190
|
+
| `theme.light` | `{ onAccent }` | built-in | v0.5.10. The light mirror of `theme.dark`. An `onAccent` set here is the filled buttons' text colour for light mode, painted as given |
|
|
190
191
|
| `texts.policyUrl` | `string` | — | v0.5.0. Cookie policy address. `http(s)` only; anything else is ignored |
|
|
191
192
|
| `texts.detailsAction` | `"policy" \| "settings" \| "hide" \| "declaration"` | see notes | v0.5.0, `declaration` in v0.5.7. What «Learn more» does. Defaults to `policy` when `policyUrl` is set, `settings` when it is not. `policy` or `declaration` without a usable URL falls back to `settings` rather than rendering a dead link |
|
|
192
193
|
| `texts.declarationUrl` | `string` | — | v0.5.7. Address of the cookie declaration page, used by `detailsAction: "declaration"`. `http(s)` only. Filled by the hosted service; the client only reads it |
|
|
@@ -274,18 +275,25 @@ accept is a dark pattern, and consent collected through one is not freely
|
|
|
274
275
|
given, so the config simply cannot express it. `settings` is independent and
|
|
275
276
|
may itself be filled.
|
|
276
277
|
|
|
277
|
-
**
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
278
|
+
**A colour you set is painted as you set it; contrast rules decide only the
|
|
279
|
+
colours you left to us.** Since v0.5.10 the **4.5:1** text rule and the **3:1**
|
|
280
|
+
border rule apply to *derived* values — the automatic text on a filled button,
|
|
281
|
+
the border and text an `outline` button takes from `theme.accent`, the link
|
|
282
|
+
colour read off the accent. A `fg`, `border` or `onAccent` you wrote yourself
|
|
283
|
+
is never repainted, however low it measures: the debug panel reports the ratio
|
|
284
|
+
and warns («контраст 4.32 — ниже рекомендуемых 4.5»), and the choice stays
|
|
285
|
+
yours. Concretely:
|
|
286
|
+
|
|
287
|
+
- a `fg` you set is painted as set and only measured against the fill behind
|
|
288
|
+
it; the *derived* text on a filled button still becomes white or `#161616`,
|
|
289
|
+
whichever contrasts more, when it would fall under 4.5:1;
|
|
290
|
+
- a `border` you set on an `outline` button is painted as set; a border
|
|
291
|
+
*derived* from `theme.accent` is darkened (light card) or lightened (dark
|
|
292
|
+
card) in small steps until it clears 3:1 against the card, so a default theme
|
|
293
|
+
can never produce a button invisible against its own card;
|
|
294
|
+
- an outline button's text, when you did not set one, is that resolved border
|
|
295
|
+
colour put through the same 4.5:1 rule — derived even when the border it came
|
|
296
|
+
from was yours.
|
|
289
297
|
|
|
290
298
|
The card is `#ffffff` in light mode and `#1c1c1e` in dark. A colour the
|
|
291
299
|
arithmetic cannot read — a CSS colour name, an `rgb()` string — is left exactly
|
|
@@ -298,7 +306,11 @@ The same arithmetic is exposed as pure functions on `ConsentKit._contrast`
|
|
|
298
306
|
instead of reimplementing them. It is present whenever `src/ck-ui.js` is loaded,
|
|
299
307
|
and it is safe to call in Node — nothing in it touches the DOM. The debug
|
|
300
308
|
panel's **Appearance** section reads it directly and reports each button's
|
|
301
|
-
resolved colours, its contrast ratio, and
|
|
309
|
+
resolved colours, its contrast ratio, and either that a derived value was
|
|
310
|
+
adjusted or that a value you set measures below the recommended floor. Each
|
|
311
|
+
resolved record carries `ratio` and `adjusted`, plus `low` (painted text under
|
|
312
|
+
4.5:1) and, for outline buttons, `borderRatio`, `borderAdjusted` and
|
|
313
|
+
`borderLow` (border under 3:1).
|
|
302
314
|
|
|
303
315
|
### The «Learn more» link
|
|
304
316
|
|
package/npm/internal-stub.mjs
CHANGED
|
@@ -28,7 +28,7 @@ export function undecidedState() {
|
|
|
28
28
|
*/
|
|
29
29
|
export function createStub() {
|
|
30
30
|
const stub = {
|
|
31
|
-
version: '0.5.
|
|
31
|
+
version: '0.5.10',
|
|
32
32
|
config: {},
|
|
33
33
|
init: function () { return undecidedState(); },
|
|
34
34
|
allowed: function (cat) { return cat === 'necessary'; },
|
package/package.json
CHANGED
package/src/ck-core.js
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
// Bare entries are used ONLY for domains dedicated entirely to tracking;
|
|
47
47
|
// where a parent domain also serves ordinary site assets (CDNs, fonts,
|
|
48
48
|
// images) the specific tracking subdomain is listed instead.
|
|
49
|
-
// Snapshot: 2026-
|
|
49
|
+
// Snapshot: 2026-09. Not exhaustive — extended as new trackers appear.
|
|
50
50
|
var HOST_DB = {
|
|
51
51
|
// --- analytics -----------------------------------------------------
|
|
52
52
|
'google-analytics.com': 'analytics',
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
'nr-data.net': 'analytics', // New Relic beacon
|
|
78
78
|
'datadoghq.com': 'analytics', // RUM
|
|
79
79
|
'datadoghq-browser-agent.com': 'analytics',
|
|
80
|
+
'vercel-insights.com': 'analytics', // Vercel Web Analytics
|
|
80
81
|
// Cloudflare Web Analytics. Subdomain only, and deliberately NOT in
|
|
81
82
|
// INFRA_DB: the rest of Cloudflare's edge is infrastructure, but this one
|
|
82
83
|
// beacon is a measurement product (§8 names it by hand for that reason).
|
|
@@ -116,6 +117,15 @@
|
|
|
116
117
|
'redditstatic.com': 'marketing',
|
|
117
118
|
'q.quora.com': 'marketing', // subdomain: quora.com is a normal site
|
|
118
119
|
'amazon-adsystem.com': 'marketing',
|
|
120
|
+
// Embedded video and social plugins. The player/plugin sets the vendor's own
|
|
121
|
+
// advertising cookies on play or render, which is an ad-profile decision the
|
|
122
|
+
// visitor has to make — not a feature the site owner merely switched on.
|
|
123
|
+
'youtube.com': 'marketing', // the player sets Google ad cookies on play
|
|
124
|
+
'youtube-nocookie.com': 'marketing', // "privacy-enhanced" still sets them once played
|
|
125
|
+
'facebook.com': 'marketing', // like/page/comments plugins and iframes
|
|
126
|
+
'instagram.com': 'marketing', // post and profile embeds
|
|
127
|
+
'cdninstagram.com': 'marketing', // the embed's own asset host
|
|
128
|
+
'sendpulse.com': 'marketing', // email/push marketing automation
|
|
119
129
|
|
|
120
130
|
// --- functional ----------------------------------------------------
|
|
121
131
|
'intercom.io': 'functional',
|
|
@@ -161,12 +171,72 @@
|
|
|
161
171
|
'places.googleapis.com': 'functional',
|
|
162
172
|
'maps.gstatic.com': 'functional',
|
|
163
173
|
|
|
174
|
+
// Vimeo — an embedded player. Unlike YouTube it does not feed an ad
|
|
175
|
+
// profile by default, so a visitor who declines functional loses the video
|
|
176
|
+
// and nothing else. player.vimeo.com is redundant under the bare entry
|
|
177
|
+
// (suffix matching covers it) and is named anyway, the way
|
|
178
|
+
// static.tildacdn.one and fonts.gstatic.com are: the embed fixtures and the
|
|
179
|
+
// docs both refer to it by its full host.
|
|
180
|
+
'vimeo.com': 'functional',
|
|
181
|
+
'player.vimeo.com': 'functional',
|
|
182
|
+
'vimeocdn.com': 'functional', // the player's own asset host
|
|
183
|
+
// Freshworks — support chat and helpdesk widgets. The widget IS the support
|
|
184
|
+
// channel, so declining functional costs the visitor the feature, not a
|
|
185
|
+
// measurement they were unaware of.
|
|
186
|
+
'freshworks.com': 'functional',
|
|
187
|
+
'freshchat.com': 'functional',
|
|
188
|
+
'freshdesk.com': 'functional',
|
|
189
|
+
// Messenger buttons and chat widgets a shop puts on its own pages. The
|
|
190
|
+
// Moldovan/Romanian market runs on these the way the western one runs on
|
|
191
|
+
// Intercom, and each is a feature the owner chose.
|
|
192
|
+
'viber.com': 'functional',
|
|
193
|
+
'telegram.org': 'functional',
|
|
194
|
+
't.me': 'functional',
|
|
195
|
+
// CRM widgets: callback forms, chat and lead capture embedded on the site.
|
|
196
|
+
// Every regional TLD is named in full — hostMatches is plain suffix
|
|
197
|
+
// matching with no pattern form, so bitrix24.ru does not cover
|
|
198
|
+
// bitrix24.com.
|
|
199
|
+
'bitrix24.ru': 'functional',
|
|
200
|
+
'bitrix24.com': 'functional',
|
|
201
|
+
'bitrix24.eu': 'functional',
|
|
202
|
+
'amocrm.ru': 'functional',
|
|
203
|
+
'amocrm.com': 'functional',
|
|
204
|
+
// Booking and form embeds — the visitor came to the page to use them.
|
|
205
|
+
'calendly.com': 'functional',
|
|
206
|
+
'typeform.com': 'functional',
|
|
207
|
+
// 999.md — the Moldovan classifieds platform. Shops embed its listing
|
|
208
|
+
// widgets; simpalsmedia.com is the group's asset host that serves them.
|
|
209
|
+
'999.md': 'functional',
|
|
210
|
+
'simpalsmedia.com': 'functional',
|
|
211
|
+
|
|
164
212
|
// --- necessary -----------------------------------------------------
|
|
165
213
|
// recaptcha.net is Google's alternate reCAPTCHA domain, served for regions
|
|
166
214
|
// where google.com is unreachable. Unlike the two path-scoped Google hosts
|
|
167
215
|
// in PATH_DB, this domain hosts NOTHING but the captcha, so the whole host
|
|
168
216
|
// is the right scope.
|
|
169
|
-
'recaptcha.net': 'necessary'
|
|
217
|
+
'recaptcha.net': 'necessary',
|
|
218
|
+
// Payment, error reporting and the page builder's own runtime. A
|
|
219
|
+
// `necessary` category is never held — allowed('necessary') is always true,
|
|
220
|
+
// so shouldBlock() lets these through before strict mode is ever consulted.
|
|
221
|
+
// What the entry buys is a NAME: the audit stops filing a checkout form or
|
|
222
|
+
// a crash reporter under «сторонние подключения без категории». This is why
|
|
223
|
+
// js.stripe.com can stay in BASE_ALLOW and gain a category here without
|
|
224
|
+
// contradiction — the allowlist decides strict mode, the category decides
|
|
225
|
+
// the report, and both answers are «let it through».
|
|
226
|
+
'sentry.io': 'necessary', // crash reporting: no visitor profile
|
|
227
|
+
'ingest.sentry.io': 'necessary', // redundant under the line above, named
|
|
228
|
+
// because the DSN host is what an owner
|
|
229
|
+
// actually sees in a report
|
|
230
|
+
'sentry-cdn.com': 'necessary',
|
|
231
|
+
'paypal.com': 'necessary',
|
|
232
|
+
'paypalobjects.com': 'necessary', // PayPal button assets
|
|
233
|
+
'paynet.md': 'necessary', // Moldovan payment gateway
|
|
234
|
+
'maib.md': 'necessary', // MAIB card processing
|
|
235
|
+
'maibank.md': 'necessary',
|
|
236
|
+
'stripe.com': 'necessary',
|
|
237
|
+
'js.stripe.com': 'necessary', // also in BASE_ALLOW; see the note above
|
|
238
|
+
'stripe.network': 'necessary',
|
|
239
|
+
'elementor.com': 'necessary' // the WordPress builder's own runtime
|
|
170
240
|
};
|
|
171
241
|
|
|
172
242
|
// Runtime overrides fed in by ConsentKit._extendHostDb(map) — the SaaS
|
|
@@ -416,6 +486,20 @@
|
|
|
416
486
|
// Google-hosted halves are path-scoped in BASE_ALLOW_PATH below, since
|
|
417
487
|
// www.google.com and www.gstatic.com cannot be waved through wholesale.
|
|
418
488
|
'hcaptcha.com',
|
|
489
|
+
// --- hosting platforms (continued) -----------------------------------
|
|
490
|
+
// Vercel and Netlify serve the site's own build output. Vercel's MEASUREMENT
|
|
491
|
+
// product is vercel-insights.com, a separate registrable domain filed above
|
|
492
|
+
// as analytics — so neither entry here shadows it (the §8 invariant that an
|
|
493
|
+
// infra host must carry no category still holds).
|
|
494
|
+
'vercel.app',
|
|
495
|
+
'vercel.com',
|
|
496
|
+
'netlify.app',
|
|
497
|
+
'netlify.com',
|
|
498
|
+
// YouTube thumbnails and player static assets. The PLAYER is marketing and
|
|
499
|
+
// sits in HOST_DB on youtube.com; this host serves only the poster image
|
|
500
|
+
// and sprites, sets nothing, and a held placeholder still wants its
|
|
501
|
+
// thumbnail.
|
|
502
|
+
'ytimg.com',
|
|
419
503
|
// --- our own service -------------------------------------------------
|
|
420
504
|
// The consent tool must not report itself as an unnamed third party.
|
|
421
505
|
'consent.ecomconsult.net'
|
|
@@ -1857,7 +1941,7 @@
|
|
|
1857
1941
|
// Public API
|
|
1858
1942
|
// ---------------------------------------------------------------------------
|
|
1859
1943
|
var ConsentKit = {
|
|
1860
|
-
version: '0.5.
|
|
1944
|
+
version: '0.5.10',
|
|
1861
1945
|
config: config,
|
|
1862
1946
|
|
|
1863
1947
|
init: function (userConfig) {
|
package/src/ck-debug.js
CHANGED
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
/* A `necessary` host is never held — allowed('necessary') is always true —
|
|
174
174
|
so it appears in no `blocked` record, and every note below would be a
|
|
175
175
|
lie about it: «раньше строки баннера» (we never wanted to hold it) or
|
|
176
|
-
«Consent Mode: без cookie
|
|
176
|
+
«Consent Mode: без cookie, но адрес…» (it is not a Consent Mode decision). §4 files
|
|
177
177
|
necessary under `ok`, and §3's four notes have no slot for it, so it
|
|
178
178
|
gets what an after-consent row gets: nothing. Sending the owner off to
|
|
179
179
|
chase __cf_bm is exactly the noise §3 exists to remove. */
|
|
@@ -286,7 +286,7 @@
|
|
|
286
286
|
// SPEC V1.12 §3 — пометка словами на каждой строке «до согласия»:
|
|
287
287
|
// что именно случилось и можно ли было это остановить.
|
|
288
288
|
whyEarly: 'раньше строки баннера — задержать не можем',
|
|
289
|
-
whyGcm: 'Consent Mode: без cookie',
|
|
289
|
+
whyGcm: 'Consent Mode: без cookie, но адрес страницы и тип браузера уходят',
|
|
290
290
|
whyHeld: 'задержан баннером',
|
|
291
291
|
whyDead: 'не ожил после согласия — проверьте, что тег помечен type="text/plain"',
|
|
292
292
|
cabinet: 'Что с этим делать — в кабинете',
|
|
@@ -322,6 +322,14 @@
|
|
|
322
322
|
btnAdjusted: 'исправлено автоматически',
|
|
323
323
|
btnOk: 'AA',
|
|
324
324
|
btnFail: 'ниже AA',
|
|
325
|
+
// 0.5.10: a colour the owner set is painted as set, so a low ratio is a
|
|
326
|
+
// WARNING, not a correction. Split around the measured number because it
|
|
327
|
+
// is the whole point of the note — «ниже рекомендуемых» with no reading
|
|
328
|
+
// tells the owner nothing about how far below they are.
|
|
329
|
+
lowTextA: 'контраст ',
|
|
330
|
+
lowTextB: ' — ниже рекомендуемых 4.5, текст может читаться хуже',
|
|
331
|
+
lowBorderA: 'обводка ',
|
|
332
|
+
lowBorderB: ' — ниже 3, кнопка плохо видна',
|
|
325
333
|
secActions: 'Действия',
|
|
326
334
|
reset: 'Сбросить согласие',
|
|
327
335
|
showPrefs: 'Показать настройки',
|
|
@@ -352,7 +360,7 @@
|
|
|
352
360
|
strict: 'strict',
|
|
353
361
|
notRevived: ' — did not come back after consent',
|
|
354
362
|
whyEarly: 'loaded before the banner line — we cannot hold it',
|
|
355
|
-
whyGcm: 'Consent Mode: no cookies',
|
|
363
|
+
whyGcm: 'Consent Mode: no cookies, but the page address and browser type are sent',
|
|
356
364
|
whyHeld: 'held back by the banner',
|
|
357
365
|
whyDead: 'did not come back after consent — check the tag is marked type="text/plain"',
|
|
358
366
|
cabinet: 'What to do about it — in your account',
|
|
@@ -388,6 +396,10 @@
|
|
|
388
396
|
btnAdjusted: 'adjusted automatically',
|
|
389
397
|
btnOk: 'AA',
|
|
390
398
|
btnFail: 'below AA',
|
|
399
|
+
lowTextA: 'contrast ',
|
|
400
|
+
lowTextB: ' — below the recommended 4.5, the text may be harder to read',
|
|
401
|
+
lowBorderA: 'border ',
|
|
402
|
+
lowBorderB: ' — below 3, the button is hard to see',
|
|
391
403
|
secActions: 'Actions',
|
|
392
404
|
reset: 'Reset consent',
|
|
393
405
|
showPrefs: 'Show preferences',
|
|
@@ -423,7 +435,7 @@
|
|
|
423
435
|
strict: 'strict',
|
|
424
436
|
notRevived: ' — nu a repornit după consimțământ',
|
|
425
437
|
whyEarly: 'încărcat înaintea liniei bannerului — nu îl putem opri',
|
|
426
|
-
whyGcm: 'Consent Mode: fără cookie
|
|
438
|
+
whyGcm: 'Consent Mode: fără cookie, dar adresa paginii și tipul browserului pleacă',
|
|
427
439
|
whyHeld: 'reținut de banner',
|
|
428
440
|
whyDead: 'nu a repornit după consimțământ — verificați că eticheta are type="text/plain"',
|
|
429
441
|
cabinet: 'Ce este de făcut — în contul dumneavoastră',
|
|
@@ -459,6 +471,10 @@
|
|
|
459
471
|
btnAdjusted: 'corectat automat',
|
|
460
472
|
btnOk: 'AA',
|
|
461
473
|
btnFail: 'sub AA',
|
|
474
|
+
lowTextA: 'contrast ',
|
|
475
|
+
lowTextB: ' — sub 4.5 recomandat, textul poate fi mai greu de citit',
|
|
476
|
+
lowBorderA: 'contur ',
|
|
477
|
+
lowBorderB: ' — sub 3, butonul se vede greu',
|
|
462
478
|
secActions: 'Acțiuni',
|
|
463
479
|
reset: 'Resetează consimțământul',
|
|
464
480
|
showPrefs: 'Arată setările',
|
|
@@ -1035,7 +1051,8 @@
|
|
|
1035
1051
|
lkTxt = lk.color + ' ' + T.btnOn + ' ' + lk.against +
|
|
1036
1052
|
' · ' + fmtRatio(lk.ratio) +
|
|
1037
1053
|
' ' + ((typeof lk.ratio === 'number' && lk.ratio >= 4.5) ? T.btnOk : T.btnFail) +
|
|
1038
|
-
(lk.adjusted ? ' · ' + T.btnAdjusted : '')
|
|
1054
|
+
(lk.adjusted ? ' · ' + T.btnAdjusted : '') +
|
|
1055
|
+
(lk.low ? ' · ' + T.lowTextA + fmtRatio(lk.ratio) + T.lowTextB : '');
|
|
1039
1056
|
}
|
|
1040
1057
|
|
|
1041
1058
|
sT.appendChild(defs([
|
|
@@ -1062,9 +1079,21 @@
|
|
|
1062
1079
|
(b.variant === 'outline'
|
|
1063
1080
|
? ' · ' + T.btnBorder + ' ' + b.border + ' (' + fmtRatio(b.borderRatio) + ')'
|
|
1064
1081
|
: '')));
|
|
1082
|
+
// «исправлено» only where something WAS in fact replaced. Since
|
|
1083
|
+
// 0.5.10 a colour the owner set is painted as set, and its shortfall
|
|
1084
|
+
// is reported as a plain warning with the measured number instead —
|
|
1085
|
+
// the panel must never claim to have corrected what it left alone.
|
|
1065
1086
|
if (b.adjusted) {
|
|
1066
1087
|
li.appendChild(el('span', { class: 'mut' }, ' · ' + T.btnAdjusted));
|
|
1067
1088
|
}
|
|
1089
|
+
if (b.low) {
|
|
1090
|
+
li.appendChild(el('span', { class: 'mut' },
|
|
1091
|
+
' · ' + T.lowTextA + fmtRatio(b.ratio) + T.lowTextB));
|
|
1092
|
+
}
|
|
1093
|
+
if (b.borderLow) {
|
|
1094
|
+
li.appendChild(el('span', { class: 'mut' },
|
|
1095
|
+
' · ' + T.lowBorderA + fmtRatio(b.borderRatio) + T.lowBorderB));
|
|
1096
|
+
}
|
|
1068
1097
|
uT.appendChild(li);
|
|
1069
1098
|
});
|
|
1070
1099
|
sT.appendChild(uT);
|
package/src/ck-ui.js
CHANGED
|
@@ -440,9 +440,13 @@
|
|
|
440
440
|
'border:1px solid var(--ck-line);border-radius:var(--ck-radius-card);overflow:hidden}',
|
|
441
441
|
'.ck-panel__head{display:flex;align-items:flex-start;gap:16px;padding:22px 24px 14px;',
|
|
442
442
|
'border-bottom:1px solid var(--ck-line)}',
|
|
443
|
+
// The text block takes the width and the close button sits at the padding
|
|
444
|
+
// edge, flush with the switches below it: without flex:1 the button
|
|
445
|
+
// followed the text's own wrapped width and floated short of the edge.
|
|
446
|
+
'.ck-panel__head>div{flex:1 1 auto;min-width:0}',
|
|
443
447
|
'.ck-panel__head h2{margin:0 0 4px;font-size:18px;font-weight:600;letter-spacing:-.01em}',
|
|
444
448
|
'.ck-panel__head p{margin:0;font-size:14px;color:var(--ck-muted)}',
|
|
445
|
-
'.ck-x{flex:none;width:36px;height:36px;border-radius:var(--ck-radius-btn);border:1px solid var(--ck-line);',
|
|
449
|
+
'.ck-x{flex:none;margin-left:auto;width:36px;height:36px;border-radius:var(--ck-radius-btn);border:1px solid var(--ck-line);',
|
|
446
450
|
'background:transparent;display:inline-flex;align-items:center;justify-content:center;color:var(--ck-muted)}',
|
|
447
451
|
'.ck-panel__body{overflow:auto;padding:6px 24px 10px;-webkit-overflow-scrolling:touch}',
|
|
448
452
|
'.ck-panel__foot{display:flex;gap:10px;flex-wrap:wrap;padding:16px 24px;',
|
|
@@ -838,10 +842,14 @@
|
|
|
838
842
|
var WHITE = '#ffffff';
|
|
839
843
|
var NEAR_BLACK = '#161616'; // SPEC §1: white or #161616, whichever contrasts more
|
|
840
844
|
|
|
841
|
-
/* SPEC §1, the TEXT rule: keep
|
|
842
|
-
|
|
845
|
+
/* SPEC §1, the TEXT rule: keep `fg` when it clears `min` against bg;
|
|
846
|
+
otherwise swap to white or #161616 — whichever wins. Unreadable input
|
|
843
847
|
(a colour name, rgb(), a missing value) is returned untouched with
|
|
844
|
-
adjusted:false
|
|
848
|
+
adjusted:false rather than guessed at.
|
|
849
|
+
|
|
850
|
+
Since 0.5.10 this is only ever handed a DERIVED colour — resolveButtonStyles
|
|
851
|
+
keeps an explicitly set one out of here entirely — so the swap can no longer
|
|
852
|
+
overwrite something the owner chose. */
|
|
845
853
|
function ensureContrast(fg, bg, min) {
|
|
846
854
|
var floor = typeof min === 'number' && min > 0 ? min : 4.5;
|
|
847
855
|
var have = contrastRatio(fg, bg);
|
|
@@ -857,7 +865,10 @@
|
|
|
857
865
|
against the CARD background, so the button is visible as a button at all.
|
|
858
866
|
Stepwise rather than a jump to black/white: a brand colour that is only
|
|
859
867
|
slightly too pale should stay recognisably the brand colour. Direction is
|
|
860
|
-
chosen by which way there is room to move — away from the card.
|
|
868
|
+
chosen by which way there is room to move — away from the card.
|
|
869
|
+
|
|
870
|
+
Since 0.5.10 only a DERIVED border reaches this: a border the owner set is
|
|
871
|
+
painted as set and merely measured. */
|
|
861
872
|
function stepToContrast(color, bg, min) {
|
|
862
873
|
var floor = typeof min === 'number' && min > 0 ? min : 3;
|
|
863
874
|
var have = contrastRatio(color, bg);
|
|
@@ -1217,7 +1228,14 @@
|
|
|
1217
1228
|
числа": nothing recomputes these numbers a second time.
|
|
1218
1229
|
|
|
1219
1230
|
`palette` is optional and lets buildThemeCss pass the ALREADY-RESOLVED
|
|
1220
|
-
accent/card colours for the mode, so a theme.dark.accent is honoured.
|
|
1231
|
+
accent/card colours for the mode, so a theme.dark.accent is honoured.
|
|
1232
|
+
|
|
1233
|
+
OWNER'S RULE (0.5.10): a colour the owner EXPLICITLY SET is painted as set.
|
|
1234
|
+
The >= 4.5 text rule and the >= 3 border rule may only decide colours the
|
|
1235
|
+
owner did NOT choose — the automatic text on a filled button, the border
|
|
1236
|
+
and text derived from the accent on an outline one. A typed colour that
|
|
1237
|
+
falls short is reported (`low` / `borderLow`) so the debug panel and the
|
|
1238
|
+
cabinet can WARN about it, never corrected behind the owner's back. */
|
|
1221
1239
|
function resolveButtonStyles(theme, mode, palette) {
|
|
1222
1240
|
var t = (theme && typeof theme === 'object') ? theme : {};
|
|
1223
1241
|
var base = mode === 'dark' ? DARK : LIGHT;
|
|
@@ -1225,9 +1243,12 @@
|
|
|
1225
1243
|
|
|
1226
1244
|
var cardBg = sanitizeCssValue('color', pal.bg, base.bg);
|
|
1227
1245
|
var accent = sanitizeCssValue('color', pal.accent, base.accent);
|
|
1228
|
-
// theme.dark.onAccent
|
|
1229
|
-
// buttons in that mode.
|
|
1246
|
+
// theme.dark.onAccent / theme.light.onAccent is the GIVEN fg for filled
|
|
1247
|
+
// buttons in that mode. When the owner set it, it counts as an explicit
|
|
1248
|
+
// colour and is painted as set; the built-in default is derived and stays
|
|
1249
|
+
// subject to the >= 4.5 rule.
|
|
1230
1250
|
var onAccent = sanitizeCssValue('color', pal.onAccent, base.onAccent);
|
|
1251
|
+
var onAccentExplicit = pal.onAccentExplicit === true;
|
|
1231
1252
|
|
|
1232
1253
|
var cfg = (t.buttons && typeof t.buttons === 'object') ? t.buttons : {};
|
|
1233
1254
|
|
|
@@ -1254,7 +1275,12 @@
|
|
|
1254
1275
|
button's icon all read the token this produces, so none of them can
|
|
1255
1276
|
drift from what the debug panel and the cabinet quote. Note the
|
|
1256
1277
|
measurement is against cardBg — an accent that is fine inside a filled
|
|
1257
|
-
button can still be unreadable on the card behind it.
|
|
1278
|
+
button can still be unreadable on the card behind it.
|
|
1279
|
+
|
|
1280
|
+
Still corrected in 0.5.10, deliberately: the ACCENT is the owner's, but
|
|
1281
|
+
the LINK colour is derived from it — nobody typed "links are this
|
|
1282
|
+
colour". The accent itself is never repainted, so the owner still sees
|
|
1283
|
+
the brand colour everywhere they actually chose it. */
|
|
1258
1284
|
var linkRes = ensureContrast(accent, cardBg, 4.5);
|
|
1259
1285
|
|
|
1260
1286
|
var out = {
|
|
@@ -1265,11 +1291,35 @@
|
|
|
1265
1291
|
color: linkRes.color,
|
|
1266
1292
|
ratio: linkRes.ratio,
|
|
1267
1293
|
adjusted: linkRes.adjusted,
|
|
1294
|
+
// A link is always derived, so this is normally false — the rule
|
|
1295
|
+
// already lifted it. It can still be true on a mid-tone custom card
|
|
1296
|
+
// where even the better of white and #161616 falls short of 4.5.
|
|
1297
|
+
low: (typeof linkRes.ratio === 'number' && linkRes.ratio < 4.5),
|
|
1268
1298
|
against: cardBg
|
|
1269
1299
|
},
|
|
1270
1300
|
buttons: {}
|
|
1271
1301
|
};
|
|
1272
1302
|
|
|
1303
|
+
/* "Did the owner explicitly set this colour?" — the gate on the whole
|
|
1304
|
+
0.5.10 rule, and it cannot be a plain `str(c.fg)` test. sanitizeCssValue
|
|
1305
|
+
silently returns the FALLBACK for a hostile or malformed value, so a
|
|
1306
|
+
value that failed the grammar would otherwise be painted as "the owner's
|
|
1307
|
+
choice" with no correction at all — the injected-colour case would lose
|
|
1308
|
+
its contrast check as well as its value. Sanitising against a sentinel
|
|
1309
|
+
tells the two apart: a value that survives is the owner's, anything else
|
|
1310
|
+
falls through to the derived path exactly as before 0.5.10. */
|
|
1311
|
+
var NOT_SET = '\x00';
|
|
1312
|
+
function explicitColor(v) {
|
|
1313
|
+
var got = sanitizeCssValue('color', v, NOT_SET);
|
|
1314
|
+
return got === NOT_SET ? null : got;
|
|
1315
|
+
}
|
|
1316
|
+
// A measured ratio is `null` for a colour the arithmetic cannot read (a CSS
|
|
1317
|
+
// name, an rgb() string). That is "no verdict", not "below the floor", so it
|
|
1318
|
+
// must not raise the warning flag — the panel would print a note with no number.
|
|
1319
|
+
function below(ratio, floor) {
|
|
1320
|
+
return typeof ratio === 'number' && ratio < floor;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1273
1323
|
for (var i = 0; i < BTN_ROLES.length; i++) {
|
|
1274
1324
|
var role = BTN_ROLES[i];
|
|
1275
1325
|
var c = (cfg[role] && typeof cfg[role] === 'object') ? cfg[role] : {};
|
|
@@ -1279,40 +1329,86 @@
|
|
|
1279
1329
|
var rec;
|
|
1280
1330
|
|
|
1281
1331
|
if (variant === 'filled') {
|
|
1282
|
-
// bg: author's, else the accent
|
|
1283
|
-
// default accent fill, then checked against the resolved bg.
|
|
1332
|
+
// bg: author's, else the accent — always painted as given either way.
|
|
1284
1333
|
var bg = sanitizeCssValue('color', c.bg, accent);
|
|
1285
|
-
var wantFg = sanitizeCssValue('color', c.fg, onAccent);
|
|
1286
|
-
var fgRes = ensureContrast(wantFg, bg, 4.5);
|
|
1287
1334
|
var bd = sanitizeCssValue('color', c.border, bg);
|
|
1335
|
+
// fg: an EXPLICIT colour (the button's own, or an owner-set onAccent
|
|
1336
|
+
// for the mode) is painted as set and only measured. Only the derived
|
|
1337
|
+
// default goes through the >= 4.5 rule.
|
|
1338
|
+
var setFg = explicitColor(c.fg) || (onAccentExplicit ? onAccent : null);
|
|
1339
|
+
var fgColor, fgRatio, fgAdjusted;
|
|
1340
|
+
if (setFg) {
|
|
1341
|
+
fgColor = setFg;
|
|
1342
|
+
fgRatio = contrastRatio(setFg, bg);
|
|
1343
|
+
fgAdjusted = false;
|
|
1344
|
+
} else {
|
|
1345
|
+
var fgRes = ensureContrast(onAccent, bg, 4.5);
|
|
1346
|
+
fgColor = fgRes.color;
|
|
1347
|
+
fgRatio = fgRes.ratio;
|
|
1348
|
+
fgAdjusted = fgRes.adjusted;
|
|
1349
|
+
}
|
|
1288
1350
|
rec = {
|
|
1289
1351
|
variant: 'filled',
|
|
1290
1352
|
bg: bg,
|
|
1291
|
-
fg:
|
|
1353
|
+
fg: fgColor,
|
|
1292
1354
|
border: bd,
|
|
1293
1355
|
borderWidth: bw,
|
|
1294
|
-
ratio:
|
|
1295
|
-
adjusted:
|
|
1356
|
+
ratio: fgRatio,
|
|
1357
|
+
adjusted: fgAdjusted,
|
|
1358
|
+
// Warn-only: true when the PAINTED text falls under 4.5:1 —
|
|
1359
|
+
// usually a typed colour (the owner's own trade-off), but also a
|
|
1360
|
+
// CORRECTED one on a mid-tone fill near #767676, where neither
|
|
1361
|
+
// white nor #161616 reaches 4.5 and the better of the two still fails.
|
|
1362
|
+
low: below(fgRatio, 4.5),
|
|
1296
1363
|
against: bg
|
|
1297
1364
|
};
|
|
1298
1365
|
} else {
|
|
1299
1366
|
// outline: transparent fill, so everything is measured against the CARD.
|
|
1300
|
-
// border
|
|
1301
|
-
// the >=
|
|
1302
|
-
|
|
1303
|
-
var
|
|
1304
|
-
var
|
|
1305
|
-
|
|
1367
|
+
// A border the owner TYPED is painted as typed and only measured; one
|
|
1368
|
+
// DERIVED from the accent is still stepped to >= 3:1 so a default-themed
|
|
1369
|
+
// button cannot vanish into the card.
|
|
1370
|
+
var setBd = explicitColor(c.border);
|
|
1371
|
+
var bdColor, bdRatio, bdAdjusted;
|
|
1372
|
+
if (setBd) {
|
|
1373
|
+
bdColor = setBd;
|
|
1374
|
+
bdRatio = contrastRatio(setBd, cardBg);
|
|
1375
|
+
bdAdjusted = false;
|
|
1376
|
+
} else {
|
|
1377
|
+
var bdRes = stepToContrast(accent, cardBg, 3);
|
|
1378
|
+
bdColor = bdRes.color;
|
|
1379
|
+
bdRatio = bdRes.ratio;
|
|
1380
|
+
bdAdjusted = bdRes.adjusted;
|
|
1381
|
+
}
|
|
1382
|
+
// Text: the owner's when set, otherwise the resolved border colour put
|
|
1383
|
+
// through the >= 4.5 rule — that text is derived even when the border
|
|
1384
|
+
// it came from was typed, so it is still corrected.
|
|
1385
|
+
var setTx = explicitColor(c.fg);
|
|
1386
|
+
var txColor, txRatio, txAdjusted;
|
|
1387
|
+
if (setTx) {
|
|
1388
|
+
txColor = setTx;
|
|
1389
|
+
txRatio = contrastRatio(setTx, cardBg);
|
|
1390
|
+
txAdjusted = false;
|
|
1391
|
+
} else {
|
|
1392
|
+
var txRes = ensureContrast(bdColor, cardBg, 4.5);
|
|
1393
|
+
txColor = txRes.color;
|
|
1394
|
+
txRatio = txRes.ratio;
|
|
1395
|
+
txAdjusted = txRes.adjusted;
|
|
1396
|
+
}
|
|
1306
1397
|
rec = {
|
|
1307
1398
|
variant: 'outline',
|
|
1308
1399
|
bg: sanitizeCssValue('color', c.bg, 'transparent'),
|
|
1309
|
-
fg:
|
|
1310
|
-
border:
|
|
1400
|
+
fg: txColor,
|
|
1401
|
+
border: bdColor,
|
|
1311
1402
|
borderWidth: bw,
|
|
1312
|
-
ratio:
|
|
1313
|
-
adjusted
|
|
1314
|
-
|
|
1315
|
-
borderAdjusted
|
|
1403
|
+
ratio: txRatio,
|
|
1404
|
+
// `adjusted` now tracks the TEXT alone. A typed border that was kept
|
|
1405
|
+
// must not make the row say «исправлено» about a colour nobody
|
|
1406
|
+
// touched; the border reports itself through borderAdjusted.
|
|
1407
|
+
adjusted: txAdjusted,
|
|
1408
|
+
low: below(txRatio, 4.5),
|
|
1409
|
+
borderRatio: bdRatio,
|
|
1410
|
+
borderAdjusted: bdAdjusted,
|
|
1411
|
+
borderLow: below(bdRatio, 3),
|
|
1316
1412
|
against: cardBg
|
|
1317
1413
|
};
|
|
1318
1414
|
}
|
|
@@ -1377,6 +1473,11 @@
|
|
|
1377
1473
|
function buildThemeCss(cfg) {
|
|
1378
1474
|
var theme = (cfg && cfg.theme) || {};
|
|
1379
1475
|
var dk = (theme.dark && typeof theme.dark === 'object') ? theme.dark : {};
|
|
1476
|
+
// theme.light is the mirror of theme.dark and exists for one value: an
|
|
1477
|
+
// onAccent the owner set for the light mode. Without it a light-mode
|
|
1478
|
+
// filled button had no way to state its text colour once for all three
|
|
1479
|
+
// buttons, and theme.dark could do what theme.light could not.
|
|
1480
|
+
var lt = (theme.light && typeof theme.light === 'object') ? theme.light : {};
|
|
1380
1481
|
|
|
1381
1482
|
var radius = resolveRadius(theme);
|
|
1382
1483
|
var font = resolveFont(theme);
|
|
@@ -1386,6 +1487,11 @@
|
|
|
1386
1487
|
sanitizeCssValue('color', theme.bg, LIGHT.bg),
|
|
1387
1488
|
sanitizeCssValue('color', theme.ink, LIGHT.ink));
|
|
1388
1489
|
if (str(theme.accent)) light.accent = sanitizeCssValue('color', theme.accent, LIGHT.accent);
|
|
1490
|
+
// An onAccent that SURVIVES the grammar is the owner's word on the filled
|
|
1491
|
+
// buttons' text and is painted as given; one that fails it falls back to
|
|
1492
|
+
// the built-in default, which is derived and still gets corrected.
|
|
1493
|
+
var ltOn = sanitizeCssValue('color', lt.onAccent, null);
|
|
1494
|
+
if (ltOn) { light.onAccent = ltOn; light.onAccentExplicit = true; }
|
|
1389
1495
|
|
|
1390
1496
|
// Dark: theme.dark overrides on top of the built-in dark palette.
|
|
1391
1497
|
// A light-only theme.accent deliberately does NOT carry into dark — the
|
|
@@ -1394,7 +1500,8 @@
|
|
|
1394
1500
|
sanitizeCssValue('color', dk.bg, DARK.bg),
|
|
1395
1501
|
sanitizeCssValue('color', dk.ink, DARK.ink));
|
|
1396
1502
|
if (str(dk.accent)) dark.accent = sanitizeCssValue('color', dk.accent, DARK.accent);
|
|
1397
|
-
|
|
1503
|
+
var dkOn = sanitizeCssValue('color', dk.onAccent, null);
|
|
1504
|
+
if (dkOn) { dark.onAccent = dkOn; dark.onAccentExplicit = true; }
|
|
1398
1505
|
|
|
1399
1506
|
// Buttons are resolved per mode against that mode's real card colour, so a
|
|
1400
1507
|
// custom dark card changes the outline border the same way a custom light
|
|
@@ -1403,6 +1510,16 @@
|
|
|
1403
1510
|
var darkBtn = resolveButtonStyles(theme, 'dark', dark);
|
|
1404
1511
|
light.__buttons = lightBtn.buttons;
|
|
1405
1512
|
dark.__buttons = darkBtn.buttons;
|
|
1513
|
+
|
|
1514
|
+
// `--ck-on-accent` paints the text of every filled surface that is NOT a
|
|
1515
|
+
// banner button — the panel's «Сохранить выбор» and the floating button —
|
|
1516
|
+
// straight from the palette. A derived onAccent therefore has to pass the
|
|
1517
|
+
// same >= 4.5 rule against the accent, or a white accent gets white text
|
|
1518
|
+
// (the owner saw exactly that). Done AFTER the buttons are resolved, so
|
|
1519
|
+
// the accept button still records `adjusted:true` for the same correction.
|
|
1520
|
+
// An onAccent the owner set is painted as set — the 0.5.10 rule.
|
|
1521
|
+
if (!light.onAccentExplicit) light.onAccent = ensureContrast(light.onAccent, light.accent, 4.5).color;
|
|
1522
|
+
if (!dark.onAccentExplicit) dark.onAccent = ensureContrast(dark.onAccent, dark.accent, 4.5).color;
|
|
1406
1523
|
light.__link = lightBtn.link;
|
|
1407
1524
|
dark.__link = darkBtn.link;
|
|
1408
1525
|
|