@ecomconsult/consentkit 0.4.0 → 0.5.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 +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 +146 -13
- package/src/ck-debug.js +120 -1
- package/src/ck-ui.js +489 -43
package/src/ck-ui.js
CHANGED
|
@@ -169,21 +169,35 @@
|
|
|
169
169
|
/* Palette tokens live in a second, generated stylesheet (see buildThemeCss).
|
|
170
170
|
They must NOT be inline host styles: an inline value outbeats every :host
|
|
171
171
|
rule, which would make the dark media query and the forced-mode class dead. */
|
|
172
|
+
/* font-FAMILY only, never the `font` shorthand: theme.font:'inherit' must
|
|
173
|
+
take the host page's typeface without also taking its size, weight and
|
|
174
|
+
line-height, which are what hold the fixed geometry together. */
|
|
172
175
|
':host{',
|
|
173
|
-
'font-family:
|
|
176
|
+
'font-family:var(--ck-font);',
|
|
174
177
|
'font-size:15px;line-height:1.5;color:var(--ck-ink);',
|
|
175
178
|
'-webkit-font-smoothing:antialiased}',
|
|
176
179
|
|
|
177
180
|
'.ck-hidden{display:none !important}',
|
|
178
181
|
|
|
179
182
|
'button{font:inherit;color:inherit;margin:0;cursor:pointer}',
|
|
180
|
-
|
|
183
|
+
/* --ck-link, not --ck-accent: every accent-coloured TEXT on the card goes
|
|
184
|
+
through the same >= 4.5:1 rule the filled buttons' labels do. A brand
|
|
185
|
+
accent picked to be legible as a BUTTON FILL (white text on it) can be
|
|
186
|
+
far too pale to read as text ON the card — a yellow accent gives a
|
|
187
|
+
perfectly good button and an invisible «Подробнее». --ck-accent stays
|
|
188
|
+
the raw brand colour and is still what paints backgrounds (the switch)
|
|
189
|
+
and the focus ring, which are non-text and answer to 3:1, not 4.5. */
|
|
190
|
+
'a{color:var(--ck-link)}',
|
|
191
|
+
/* «Подробнее» as a settings-opening button must read as the link it
|
|
192
|
+
replaces, not as a third action button. */
|
|
193
|
+
'.ck-linkbtn{background:none;border:0;padding:0;font:inherit;font-size:inherit;',
|
|
194
|
+
'color:var(--ck-link);text-decoration:underline;cursor:pointer}',
|
|
181
195
|
':focus-visible{outline:2px solid var(--ck-accent);outline-offset:2px;border-radius:4px}',
|
|
182
196
|
|
|
183
197
|
/* ---- banner ---- */
|
|
184
198
|
'.ck-scrim{position:fixed;inset:0;background:rgba(16,20,30,.28);z-index:2147483000;pointer-events:none}',
|
|
185
199
|
'.ck-banner{position:fixed;z-index:2147483001;background:var(--ck-bg);color:var(--ck-ink);',
|
|
186
|
-
'border:1px solid var(--ck-line);border-radius:var(--ck-radius);pointer-events:auto}',
|
|
200
|
+
'border:1px solid var(--ck-line);border-radius:var(--ck-radius-card);pointer-events:auto}',
|
|
187
201
|
'.ck-banner--bar{left:16px;right:16px;padding:18px 20px;',
|
|
188
202
|
'display:flex;gap:20px;align-items:center;flex-wrap:wrap}',
|
|
189
203
|
'.ck-banner--bar.ck-pos-bottom{bottom:16px}',
|
|
@@ -191,22 +205,29 @@
|
|
|
191
205
|
'.ck-banner--modal{top:50%;left:50%;transform:translate(-50%,-50%);',
|
|
192
206
|
'width:min(560px,calc(100vw - 32px));max-height:calc(100vh - 32px);overflow:auto;padding:24px}',
|
|
193
207
|
|
|
194
|
-
/* box:
|
|
195
|
-
|
|
196
|
-
'
|
|
208
|
+
/* box: the SPEC V1.6 reference card — max 540 wide, 24px padding, corner
|
|
209
|
+
anchored, no scrim. */
|
|
210
|
+
'.ck-banner--box{width:min(540px,calc(100vw - 32px));max-height:calc(100vh - 32px);',
|
|
211
|
+
'overflow:auto;padding:24px;display:block}',
|
|
197
212
|
'.ck-banner--box.ck-pos-bottom-right{bottom:16px;right:16px}',
|
|
198
213
|
'.ck-banner--box.ck-pos-bottom-left{bottom:16px;left:16px}',
|
|
199
214
|
/* Vertical layouts: the copy ends with the "learn more" link, so the gap
|
|
200
215
|
below it has to clear a text baseline, not just a block edge — 16px
|
|
201
216
|
reads as attached to the buttons. */
|
|
202
217
|
'.ck-banner.ck-banner--box p,.ck-banner.ck-banner--modal p{margin-bottom:22px}',
|
|
203
|
-
/*
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
'.ck-banner--box .ck-
|
|
218
|
+
/* Reference row: three equal buttons, 8px gap, wrapping to a column under
|
|
219
|
+
560px (the media query below). Keyed off the ROLE classes, never the
|
|
220
|
+
variant ones — since 0.5.0 `settings` may itself be filled. */
|
|
221
|
+
'.ck-banner--box .ck-actions{display:flex;gap:8px;flex-wrap:wrap}',
|
|
222
|
+
'.ck-banner--box .ck-btn{min-width:0;flex:1 1 0}',
|
|
207
223
|
'.ck-banner__body{flex:1 1 320px;min-width:0}',
|
|
208
224
|
'.ck-banner h2{margin:0 0 6px;font-size:17px;font-weight:600;letter-spacing:-.01em}',
|
|
209
225
|
'.ck-banner p{margin:0;color:var(--ck-muted);font-size:14px}',
|
|
226
|
+
/* Box overrides sit AFTER the generic .ck-banner rules on purpose: the two
|
|
227
|
+
selectors have equal specificity, so source order is what decides, and
|
|
228
|
+
placing these with the rest of the box block silently lost. The SPEC
|
|
229
|
+
V1.6 reference calls for a 20/700 heading here. */
|
|
230
|
+
'.ck-banner--box h2{font-size:20px;font-weight:700;margin:0 0 8px}',
|
|
210
231
|
'.ck-banner--modal p{margin-bottom:20px}',
|
|
211
232
|
'.ck-banner__more{white-space:nowrap}',
|
|
212
233
|
|
|
@@ -215,9 +236,27 @@
|
|
|
215
236
|
'.ck-banner--modal .ck-actions{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr))}',
|
|
216
237
|
'.ck-btn{display:inline-flex;align-items:center;justify-content:center;text-align:center;',
|
|
217
238
|
'min-width:150px;min-height:44px;padding:11px 18px;font-size:14px;font-weight:600;line-height:1.2;',
|
|
218
|
-
'border-radius:var(--ck-radius);border:1px solid transparent;background:transparent;flex:1 1 auto}',
|
|
239
|
+
'border-radius:var(--ck-radius-btn);border:1px solid transparent;background:transparent;flex:1 1 auto}',
|
|
240
|
+
/* Role classes read their own resolved tokens, so filled/outline is a VALUE
|
|
241
|
+
change, not a class swap — a theme.buttons edit restyles in place and
|
|
242
|
+
needs no remount. box-sizing:border-box is global, so borderWidth:2 does
|
|
243
|
+
not change the outer size: the equal-buttons invariant survives it. */
|
|
244
|
+
'.ck-btn--accept{background:var(--ck-accept-bg);color:var(--ck-accept-fg);',
|
|
245
|
+
'border-color:var(--ck-accept-bd);border-width:var(--ck-accept-bw)}',
|
|
246
|
+
'.ck-btn--reject{background:var(--ck-reject-bg);color:var(--ck-reject-fg);',
|
|
247
|
+
'border-color:var(--ck-reject-bd);border-width:var(--ck-reject-bw)}',
|
|
248
|
+
'.ck-btn--settings{background:var(--ck-settings-bg);color:var(--ck-settings-fg);',
|
|
249
|
+
'border-color:var(--ck-settings-bd);border-width:var(--ck-settings-bw)}',
|
|
250
|
+
/* Kept for the preferences panel's own footer, which is not themable. */
|
|
219
251
|
'.ck-btn--filled{background:var(--ck-accent);border-color:var(--ck-accent);color:var(--ck-on-accent)}',
|
|
220
|
-
|
|
252
|
+
/* Live, not a fallback: these paint the preferences panel's own Accept
|
|
253
|
+
all / Reject all. Transparent fill means the LABEL sits on the card, so
|
|
254
|
+
it takes --ck-link like every other accent-coloured text; the BORDER is
|
|
255
|
+
non-text and answers to 3:1, so it keeps the raw accent. The footer's
|
|
256
|
+
background is --ck-soft rather than --ck-bg, but soft is a 5% ink mix of
|
|
257
|
+
the card — the ratio difference is well inside the noise, and a second
|
|
258
|
+
link token per background would break «один код — одни числа». */
|
|
259
|
+
'.ck-btn--outline{background:transparent;border-color:var(--ck-accent);color:var(--ck-link)}',
|
|
221
260
|
'.ck-btn--ghost{min-width:0;border-color:var(--ck-line);color:var(--ck-ink);font-weight:500}',
|
|
222
261
|
|
|
223
262
|
/* ---- panel ---- */
|
|
@@ -225,12 +264,12 @@
|
|
|
225
264
|
'.ck-panel{position:fixed;z-index:2147483003;top:50%;left:50%;transform:translate(-50%,-50%);',
|
|
226
265
|
'width:min(620px,calc(100vw - 32px));max-height:calc(100vh - 48px);',
|
|
227
266
|
'display:flex;flex-direction:column;background:var(--ck-bg);color:var(--ck-ink);',
|
|
228
|
-
'border:1px solid var(--ck-line);border-radius:var(--ck-radius);overflow:hidden}',
|
|
267
|
+
'border:1px solid var(--ck-line);border-radius:var(--ck-radius-card);overflow:hidden}',
|
|
229
268
|
'.ck-panel__head{display:flex;align-items:flex-start;gap:16px;padding:22px 24px 14px;',
|
|
230
269
|
'border-bottom:1px solid var(--ck-line)}',
|
|
231
270
|
'.ck-panel__head h2{margin:0 0 4px;font-size:18px;font-weight:600;letter-spacing:-.01em}',
|
|
232
271
|
'.ck-panel__head p{margin:0;font-size:14px;color:var(--ck-muted)}',
|
|
233
|
-
'.ck-x{flex:none;width:36px;height:36px;border-radius:var(--ck-radius);border:1px solid var(--ck-line);',
|
|
272
|
+
'.ck-x{flex:none;width:36px;height:36px;border-radius:var(--ck-radius-btn);border:1px solid var(--ck-line);',
|
|
234
273
|
'background:transparent;display:inline-flex;align-items:center;justify-content:center;color:var(--ck-muted)}',
|
|
235
274
|
'.ck-panel__body{overflow:auto;padding:6px 24px 10px;-webkit-overflow-scrolling:touch}',
|
|
236
275
|
'.ck-panel__foot{display:flex;gap:10px;flex-wrap:wrap;padding:16px 24px;',
|
|
@@ -258,13 +297,13 @@
|
|
|
258
297
|
|
|
259
298
|
/* ---- cookie table ---- */
|
|
260
299
|
'.ck-det{margin-top:12px}',
|
|
261
|
-
'.ck-det>summary{cursor:pointer;font-size:13px;color:var(--ck-
|
|
300
|
+
'.ck-det>summary{cursor:pointer;font-size:13px;color:var(--ck-link);',
|
|
262
301
|
'list-style:none;display:inline-flex;align-items:center;gap:6px;padding:2px 0}',
|
|
263
302
|
'.ck-det>summary::-webkit-details-marker{display:none}',
|
|
264
303
|
'.ck-det>summary::before{content:"";width:0;height:0;border:4px solid transparent;',
|
|
265
304
|
'border-left-color:currentColor;border-right:0}',
|
|
266
305
|
'.ck-det[open]>summary::before{transform:rotate(90deg)}',
|
|
267
|
-
'.ck-tablewrap{margin-top:8px;overflow-x:auto;border:1px solid var(--ck-line);border-radius:var(--ck-radius)}',
|
|
306
|
+
'.ck-tablewrap{margin-top:8px;overflow-x:auto;border:1px solid var(--ck-line);border-radius:var(--ck-radius-card)}',
|
|
268
307
|
'table{border-collapse:collapse;width:100%;font-size:13px;min-width:420px}',
|
|
269
308
|
'th,td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--ck-line);vertical-align:top}',
|
|
270
309
|
'thead th{background:var(--ck-soft);font-weight:600;font-size:12px;color:var(--ck-muted);white-space:nowrap}',
|
|
@@ -274,7 +313,7 @@
|
|
|
274
313
|
|
|
275
314
|
/* ---- floating button ---- */
|
|
276
315
|
'.ck-fab{position:fixed;left:16px;bottom:16px;z-index:2147482999;width:48px;height:48px;',
|
|
277
|
-
'border-radius:50%;border:1px solid var(--ck-line);background:var(--ck-bg);color:var(--ck-
|
|
316
|
+
'border-radius:50%;border:1px solid var(--ck-line);background:var(--ck-bg);color:var(--ck-link);',
|
|
278
317
|
'display:inline-flex;align-items:center;justify-content:center;padding:0}',
|
|
279
318
|
'.ck-fab svg{width:24px;height:24px;display:block}',
|
|
280
319
|
|
|
@@ -285,7 +324,10 @@
|
|
|
285
324
|
'.ck-banner--bar{flex-direction:column;align-items:stretch}',
|
|
286
325
|
'.ck-banner--bar .ck-banner__body{display:contents}',
|
|
287
326
|
'.ck-banner--bar .ck-banner__body>*{order:1}',
|
|
288
|
-
'.ck-banner--bar .ck-actions{order:2}
|
|
327
|
+
'.ck-banner--bar .ck-actions{order:2}',
|
|
328
|
+
/* Reference: the box's button row becomes a column under 560px. */
|
|
329
|
+
'.ck-banner--box .ck-actions{flex-direction:column}',
|
|
330
|
+
'.ck-banner--box .ck-btn{flex:1 1 auto;width:100%}}',
|
|
289
331
|
|
|
290
332
|
'@media (prefers-reduced-motion: no-preference){',
|
|
291
333
|
'.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}}'
|
|
@@ -419,20 +461,138 @@
|
|
|
419
461
|
/* ----------------------------------------------------------------- theme */
|
|
420
462
|
|
|
421
463
|
// Built-in palettes. Dark values are picked for >= 4.5:1 text contrast.
|
|
422
|
-
|
|
464
|
+
// Card backgrounds follow SPEC V1.6 §1: light #ffffff, dark #1c1c1e. The dark
|
|
465
|
+
// card is a NEUTRAL grey, so line/soft/muted below are neutral too — the old
|
|
466
|
+
// blue-tinted #333C4F/#232B3A were derived from the old blue-tinted #1A202D
|
|
467
|
+
// and would read as a colour cast against #1c1c1e.
|
|
468
|
+
var DEFAULT_RADIUS = { card: 16, button: 8 };
|
|
469
|
+
var RADIUS_MIN = 0, RADIUS_MAX = 32;
|
|
470
|
+
var SYSTEM_FONT = 'system-ui,-apple-system,"Segoe UI",Roboto,sans-serif';
|
|
471
|
+
|
|
423
472
|
var LIGHT = {
|
|
424
|
-
bg: '#
|
|
473
|
+
bg: '#ffffff', ink: '#1B2437', accent: '#2B50D8', onAccent: '#ffffff',
|
|
425
474
|
muted: '#5b6478', line: '#dfe3ea', soft: '#f4f6f9'
|
|
426
475
|
};
|
|
427
476
|
var DARK = {
|
|
428
|
-
bg: '#
|
|
429
|
-
muted: '#
|
|
477
|
+
bg: '#1c1c1e', ink: '#E9E9EB', accent: '#7B96F0', onAccent: '#12182A',
|
|
478
|
+
muted: '#A0A0A8', line: '#3A3A3C', soft: '#2C2C2E'
|
|
430
479
|
};
|
|
431
480
|
|
|
432
481
|
function str(v) {
|
|
433
482
|
return (typeof v === 'string' && v.trim()) ? v.trim() : null;
|
|
434
483
|
}
|
|
435
484
|
|
|
485
|
+
/* ------------------------------------------------------------- contrast */
|
|
486
|
+
|
|
487
|
+
/* WCAG 2.1 relative luminance and contrast ratio. Pure, hex-only: everything
|
|
488
|
+
that reaches them has already passed sanitizeCssValue(), and a value that
|
|
489
|
+
is not a hex triple (a colour name, an rgb() string, a color-mix()) simply
|
|
490
|
+
cannot be measured here — those return null and every caller then leaves
|
|
491
|
+
the colour exactly as the author wrote it rather than guessing. Silently
|
|
492
|
+
"fixing" a colour we cannot read would be worse than not fixing it. */
|
|
493
|
+
|
|
494
|
+
// '#abc' | '#aabbcc' | '#aabbccdd' -> [r,g,b] 0..255, or null.
|
|
495
|
+
function parseHex(hex) {
|
|
496
|
+
var s = str(hex);
|
|
497
|
+
if (!s || s.charAt(0) !== '#') return null;
|
|
498
|
+
s = s.slice(1);
|
|
499
|
+
if (s.length === 3 || s.length === 4) {
|
|
500
|
+
s = s.charAt(0) + s.charAt(0) + s.charAt(1) + s.charAt(1) + s.charAt(2) + s.charAt(2);
|
|
501
|
+
} else if (s.length === 6 || s.length === 8) {
|
|
502
|
+
s = s.slice(0, 6);
|
|
503
|
+
} else {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
if (!/^[0-9a-fA-F]{6}$/.test(s)) return null;
|
|
507
|
+
return [
|
|
508
|
+
parseInt(s.slice(0, 2), 16),
|
|
509
|
+
parseInt(s.slice(2, 4), 16),
|
|
510
|
+
parseInt(s.slice(4, 6), 16)
|
|
511
|
+
];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function toHex(rgb) {
|
|
515
|
+
var out = '#';
|
|
516
|
+
for (var i = 0; i < 3; i++) {
|
|
517
|
+
var v = Math.round(rgb[i]);
|
|
518
|
+
if (v < 0) v = 0; else if (v > 255) v = 255;
|
|
519
|
+
out += (v + 0x100).toString(16).slice(1);
|
|
520
|
+
}
|
|
521
|
+
return out;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// WCAG 2.1: L = 0.2126R + 0.7152G + 0.0722B over linearised channels.
|
|
525
|
+
function relativeLuminance(hex) {
|
|
526
|
+
var rgb = parseHex(hex);
|
|
527
|
+
if (!rgb) return null;
|
|
528
|
+
var lin = [];
|
|
529
|
+
for (var i = 0; i < 3; i++) {
|
|
530
|
+
var c = rgb[i] / 255;
|
|
531
|
+
lin.push(c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
|
|
532
|
+
}
|
|
533
|
+
return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// (L1 + 0.05) / (L2 + 0.05), lighter first. null when either side is unreadable.
|
|
537
|
+
function contrastRatio(a, b) {
|
|
538
|
+
var la = relativeLuminance(a);
|
|
539
|
+
var lb = relativeLuminance(b);
|
|
540
|
+
if (la === null || lb === null) return null;
|
|
541
|
+
var hi = la > lb ? la : lb;
|
|
542
|
+
var lo = la > lb ? lb : la;
|
|
543
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
var WHITE = '#ffffff';
|
|
547
|
+
var NEAR_BLACK = '#161616'; // SPEC §1: white or #161616, whichever contrasts more
|
|
548
|
+
|
|
549
|
+
/* SPEC §1, the TEXT rule: keep the author's fg when it clears `min` against
|
|
550
|
+
bg; otherwise swap to white or #161616 — whichever wins. Unreadable input
|
|
551
|
+
(a colour name, rgb(), a missing value) is returned untouched with
|
|
552
|
+
adjusted:false: the author sees their own colour, not a silent rewrite. */
|
|
553
|
+
function ensureContrast(fg, bg, min) {
|
|
554
|
+
var floor = typeof min === 'number' && min > 0 ? min : 4.5;
|
|
555
|
+
var have = contrastRatio(fg, bg);
|
|
556
|
+
if (have === null) return { color: fg, adjusted: false, ratio: have };
|
|
557
|
+
if (have >= floor) return { color: fg, adjusted: false, ratio: have };
|
|
558
|
+
var rw = contrastRatio(WHITE, bg);
|
|
559
|
+
var rb = contrastRatio(NEAR_BLACK, bg);
|
|
560
|
+
var pick = (rw !== null && rb !== null && rw >= rb) ? WHITE : NEAR_BLACK;
|
|
561
|
+
return { color: pick, adjusted: true, ratio: contrastRatio(pick, bg) };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/* SPEC §1, the BORDER rule: an outline button's border must clear >= 3:1
|
|
565
|
+
against the CARD background, so the button is visible as a button at all.
|
|
566
|
+
Stepwise rather than a jump to black/white: a brand colour that is only
|
|
567
|
+
slightly too pale should stay recognisably the brand colour. Direction is
|
|
568
|
+
chosen by which way there is room to move — away from the card. */
|
|
569
|
+
function stepToContrast(color, bg, min) {
|
|
570
|
+
var floor = typeof min === 'number' && min > 0 ? min : 3;
|
|
571
|
+
var have = contrastRatio(color, bg);
|
|
572
|
+
if (have === null) return { color: color, adjusted: false, ratio: have };
|
|
573
|
+
if (have >= floor) return { color: color, adjusted: false, ratio: have };
|
|
574
|
+
|
|
575
|
+
var rgb = parseHex(color);
|
|
576
|
+
var bgLum = relativeLuminance(bg);
|
|
577
|
+
// Light card -> darken the border; dark card -> lighten it.
|
|
578
|
+
var darken = bgLum > 0.5;
|
|
579
|
+
var cur = [rgb[0], rgb[1], rgb[2]];
|
|
580
|
+
// 5% steps of the remaining range; 40 steps always reaches pure black/white.
|
|
581
|
+
for (var i = 0; i < 40; i++) {
|
|
582
|
+
for (var c = 0; c < 3; c++) {
|
|
583
|
+
cur[c] = darken ? cur[c] - (cur[c] * 0.06) - 2 : cur[c] + ((255 - cur[c]) * 0.06) + 2;
|
|
584
|
+
if (cur[c] < 0) cur[c] = 0; else if (cur[c] > 255) cur[c] = 255;
|
|
585
|
+
}
|
|
586
|
+
var next = toHex(cur);
|
|
587
|
+
var r = contrastRatio(next, bg);
|
|
588
|
+
if (r !== null && r >= floor) return { color: next, adjusted: true, ratio: r };
|
|
589
|
+
}
|
|
590
|
+
var last = toHex(cur);
|
|
591
|
+
return { color: last, adjusted: true, ratio: contrastRatio(last, bg) };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/* ------------------------------------------------------- config grammar */
|
|
595
|
+
|
|
436
596
|
/* Config values are interpolated into the TEXT of a generated stylesheet, so
|
|
437
597
|
an unvalidated value can close the declaration and open rules of its own
|
|
438
598
|
("10px;}.ck-btn--filled{display:none" hides "Reject all"). config.theme is
|
|
@@ -443,10 +603,11 @@
|
|
|
443
603
|
fit — a broken colour is a cosmetic bug, an injected rule is a defacement
|
|
444
604
|
and can strip the reject button, which is a consent-validity problem.
|
|
445
605
|
|
|
446
|
-
Sanitising happens at the entry points
|
|
447
|
-
|
|
448
|
-
color-mix() they are already clean, and the
|
|
449
|
-
generated color-mix() strings must not be
|
|
606
|
+
Sanitising happens at the entry points (the config reads in
|
|
607
|
+
resolveButtonStyles and buildThemeCss), not in tokenBlock(): by the time
|
|
608
|
+
values reach derive() and color-mix() they are already clean, and the
|
|
609
|
+
built-in constants and generated color-mix()/hex strings must not be
|
|
610
|
+
re-validated by this grammar. */
|
|
450
611
|
var RE_CSS = {
|
|
451
612
|
// #RGB / #RRGGBB / #RRGGBBAA (and #RGBA), rgb()/rgba()/hsl()/hsla() with
|
|
452
613
|
// numbers, commas, spaces, %, decimals and slashes, or a bare colour name.
|
|
@@ -465,8 +626,179 @@
|
|
|
465
626
|
return v;
|
|
466
627
|
}
|
|
467
628
|
|
|
629
|
+
// theme.radius: {card,button} in px, clamped 0..32. A bare string or number
|
|
630
|
+
// is the pre-0.5.0 form and still works: it sets the CARD radius, the button
|
|
631
|
+
// keeps its own default. Anything unreadable falls back to the default.
|
|
632
|
+
function clampRadius(v, fallback) {
|
|
633
|
+
var n;
|
|
634
|
+
if (typeof v === 'number') n = v;
|
|
635
|
+
else if (typeof v === 'string' && /^\d+(\.\d+)?(px)?$/.test(v.trim())) n = parseFloat(v);
|
|
636
|
+
else return fallback;
|
|
637
|
+
if (!isFinite(n)) return fallback;
|
|
638
|
+
if (n < RADIUS_MIN) n = RADIUS_MIN;
|
|
639
|
+
if (n > RADIUS_MAX) n = RADIUS_MAX;
|
|
640
|
+
return Math.round(n);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function resolveRadius(theme) {
|
|
644
|
+
var r = theme && theme.radius;
|
|
645
|
+
if (r && typeof r === 'object') {
|
|
646
|
+
return {
|
|
647
|
+
card: clampRadius(r.card, DEFAULT_RADIUS.card),
|
|
648
|
+
button: clampRadius(r.button, DEFAULT_RADIUS.button)
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
// Legacy scalar ('10px'): card only.
|
|
652
|
+
if (typeof r === 'string' || typeof r === 'number') {
|
|
653
|
+
return { card: clampRadius(r, DEFAULT_RADIUS.card), button: DEFAULT_RADIUS.button };
|
|
654
|
+
}
|
|
655
|
+
return { card: DEFAULT_RADIUS.card, button: DEFAULT_RADIUS.button };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// theme.font: 'system' keeps the pre-0.5.0 stack; anything else (including the
|
|
659
|
+
// default) inherits the host page's family. Sizes stay explicit either way —
|
|
660
|
+
// this is font-FAMILY, deliberately not the `font` shorthand, which would drag
|
|
661
|
+
// in the page's size, weight and line-height and break the fixed geometry.
|
|
662
|
+
function resolveFont(theme) {
|
|
663
|
+
return (theme && theme.font === 'system') ? SYSTEM_FONT : 'inherit';
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/* --------------------------------------------------- button resolution */
|
|
667
|
+
|
|
668
|
+
var BTN_ROLES = ['accept', 'reject', 'settings'];
|
|
669
|
+
var DEFAULT_VARIANT = { accept: 'filled', reject: 'filled', settings: 'outline' };
|
|
670
|
+
|
|
671
|
+
var warnedVariant = false;
|
|
672
|
+
// Debug-only, once per page: a config error worth surfacing to whoever is
|
|
673
|
+
// looking, but never console noise on a visitor's production page. Checked
|
|
674
|
+
// lazily because ck-debug.js loads AFTER this file and flips `active` later.
|
|
675
|
+
function warnVariantMismatch() {
|
|
676
|
+
if (warnedVariant) return;
|
|
677
|
+
var dbg = (typeof window !== 'undefined') && window.__ckDebug;
|
|
678
|
+
if (!dbg || dbg.active !== true) return;
|
|
679
|
+
warnedVariant = true;
|
|
680
|
+
try {
|
|
681
|
+
console.warn('[ConsentKit] theme.buttons.accept.variant and .reject.variant differ. ' +
|
|
682
|
+
'Accept and reject must look identical (equal-buttons invariant, SPEC §1) — ' +
|
|
683
|
+
'accept\'s variant is used for both.');
|
|
684
|
+
} catch (e) { /* noop */ }
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/* Resolve the three buttons for ONE mode into concrete, contrast-checked
|
|
688
|
+
colours. Pure: takes the theme object and 'light'|'dark', returns plain
|
|
689
|
+
data. This is the single source of truth the generated stylesheet, the
|
|
690
|
+
debug panel and (per SPEC §3) the cabinet all read — "один код — одни
|
|
691
|
+
числа": nothing recomputes these numbers a second time.
|
|
692
|
+
|
|
693
|
+
`palette` is optional and lets buildThemeCss pass the ALREADY-RESOLVED
|
|
694
|
+
accent/card colours for the mode, so a theme.dark.accent is honoured. */
|
|
695
|
+
function resolveButtonStyles(theme, mode, palette) {
|
|
696
|
+
var t = (theme && typeof theme === 'object') ? theme : {};
|
|
697
|
+
var base = mode === 'dark' ? DARK : LIGHT;
|
|
698
|
+
var pal = (palette && typeof palette === 'object') ? palette : {};
|
|
699
|
+
|
|
700
|
+
var cardBg = sanitizeCssValue('color', pal.bg, base.bg);
|
|
701
|
+
var accent = sanitizeCssValue('color', pal.accent, base.accent);
|
|
702
|
+
// theme.dark.onAccent stays meaningful: it is the GIVEN fg for filled
|
|
703
|
+
// buttons in that mode. The >= 4.5 rule may still override it.
|
|
704
|
+
var onAccent = sanitizeCssValue('color', pal.onAccent, base.onAccent);
|
|
705
|
+
|
|
706
|
+
var cfg = (t.buttons && typeof t.buttons === 'object') ? t.buttons : {};
|
|
707
|
+
|
|
708
|
+
// Equal-buttons invariant (SPEC §1): accept and reject share size and
|
|
709
|
+
// weight, so they must share the variant too. When they disagree, accept
|
|
710
|
+
// wins — silently for a visitor, loudly in debug mode.
|
|
711
|
+
function variantOf(role) {
|
|
712
|
+
var c = cfg[role];
|
|
713
|
+
var v = c && c.variant;
|
|
714
|
+
return (v === 'filled' || v === 'outline') ? v : DEFAULT_VARIANT[role];
|
|
715
|
+
}
|
|
716
|
+
var acceptVariant = variantOf('accept');
|
|
717
|
+
var rejectVariant = variantOf('reject');
|
|
718
|
+
if (acceptVariant !== rejectVariant) warnVariantMismatch();
|
|
719
|
+
|
|
720
|
+
var variants = {
|
|
721
|
+
accept: acceptVariant,
|
|
722
|
+
reject: acceptVariant, // invariant: always accept's
|
|
723
|
+
settings: variantOf('settings')
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
/* The accent as TEXT on the card. Same >= 4.5 rule, same function, one
|
|
727
|
+
number: links, «Подробнее», the cookie-table summary and the floating
|
|
728
|
+
button's icon all read the token this produces, so none of them can
|
|
729
|
+
drift from what the debug panel and the cabinet quote. Note the
|
|
730
|
+
measurement is against cardBg — an accent that is fine inside a filled
|
|
731
|
+
button can still be unreadable on the card behind it. */
|
|
732
|
+
var linkRes = ensureContrast(accent, cardBg, 4.5);
|
|
733
|
+
|
|
734
|
+
var out = {
|
|
735
|
+
mode: mode,
|
|
736
|
+
cardBg: cardBg,
|
|
737
|
+
accent: accent,
|
|
738
|
+
link: {
|
|
739
|
+
color: linkRes.color,
|
|
740
|
+
ratio: linkRes.ratio,
|
|
741
|
+
adjusted: linkRes.adjusted,
|
|
742
|
+
against: cardBg
|
|
743
|
+
},
|
|
744
|
+
buttons: {}
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
for (var i = 0; i < BTN_ROLES.length; i++) {
|
|
748
|
+
var role = BTN_ROLES[i];
|
|
749
|
+
var c = (cfg[role] && typeof cfg[role] === 'object') ? cfg[role] : {};
|
|
750
|
+
var variant = variants[role];
|
|
751
|
+
|
|
752
|
+
var bw = (c.borderWidth === 2 || c.borderWidth === '2') ? 2 : 1;
|
|
753
|
+
var rec;
|
|
754
|
+
|
|
755
|
+
if (variant === 'filled') {
|
|
756
|
+
// bg: author's, else the accent. fg: author's, else onAccent for the
|
|
757
|
+
// default accent fill, then checked against the resolved bg.
|
|
758
|
+
var bg = sanitizeCssValue('color', c.bg, accent);
|
|
759
|
+
var wantFg = sanitizeCssValue('color', c.fg, onAccent);
|
|
760
|
+
var fgRes = ensureContrast(wantFg, bg, 4.5);
|
|
761
|
+
var bd = sanitizeCssValue('color', c.border, bg);
|
|
762
|
+
rec = {
|
|
763
|
+
variant: 'filled',
|
|
764
|
+
bg: bg,
|
|
765
|
+
fg: fgRes.color,
|
|
766
|
+
border: bd,
|
|
767
|
+
borderWidth: bw,
|
|
768
|
+
ratio: fgRes.ratio,
|
|
769
|
+
adjusted: fgRes.adjusted,
|
|
770
|
+
against: bg
|
|
771
|
+
};
|
|
772
|
+
} else {
|
|
773
|
+
// outline: transparent fill, so everything is measured against the CARD.
|
|
774
|
+
// border >= 3:1 with the card (stepped), text = the border colour with
|
|
775
|
+
// the >= 4.5 rule against the card.
|
|
776
|
+
var wantBd = sanitizeCssValue('color', c.border, accent);
|
|
777
|
+
var bdRes = stepToContrast(wantBd, cardBg, 3);
|
|
778
|
+
var wantTx = str(c.fg) ? sanitizeCssValue('color', c.fg, bdRes.color) : bdRes.color;
|
|
779
|
+
var txRes = ensureContrast(wantTx, cardBg, 4.5);
|
|
780
|
+
rec = {
|
|
781
|
+
variant: 'outline',
|
|
782
|
+
bg: sanitizeCssValue('color', c.bg, 'transparent'),
|
|
783
|
+
fg: txRes.color,
|
|
784
|
+
border: bdRes.color,
|
|
785
|
+
borderWidth: bw,
|
|
786
|
+
ratio: txRes.ratio,
|
|
787
|
+
adjusted: txRes.adjusted || bdRes.adjusted,
|
|
788
|
+
borderRatio: bdRes.ratio,
|
|
789
|
+
borderAdjusted: bdRes.adjusted,
|
|
790
|
+
against: cardBg
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
out.buttons[role] = rec;
|
|
794
|
+
}
|
|
795
|
+
return out;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/* ------------------------------------------------------------- stylesheet */
|
|
799
|
+
|
|
468
800
|
// One :host{} block of custom properties for a resolved palette.
|
|
469
|
-
function tokenBlock(sel, p,
|
|
801
|
+
function tokenBlock(sel, p, extra) {
|
|
470
802
|
var d = [
|
|
471
803
|
'--ck-bg:' + p.bg,
|
|
472
804
|
'--ck-ink:' + p.ink,
|
|
@@ -476,7 +808,28 @@
|
|
|
476
808
|
'--ck-line:' + p.line,
|
|
477
809
|
'--ck-soft:' + p.soft
|
|
478
810
|
];
|
|
479
|
-
|
|
811
|
+
// Per-button tokens: the ROLE classes in the static sheet read these, so a
|
|
812
|
+
// filled/outline change is a value change and never a class swap. That is
|
|
813
|
+
// what keeps a theme.buttons edit on the applyTheme()-only path — no
|
|
814
|
+
// remount, and signature() need not know about buttons at all.
|
|
815
|
+
// Resolved by resolveButtonStyles alongside the buttons, threaded in the
|
|
816
|
+
// same way as __buttons: one pass per mode, so the light and dark blocks
|
|
817
|
+
// each carry their own contrast-checked link colour.
|
|
818
|
+
if (p.__link) d.push('--ck-link:' + p.__link.color);
|
|
819
|
+
var btns = p.__buttons;
|
|
820
|
+
if (btns) {
|
|
821
|
+
for (var i = 0; i < BTN_ROLES.length; i++) {
|
|
822
|
+
var role = BTN_ROLES[i];
|
|
823
|
+
var b = btns[role];
|
|
824
|
+
d.push('--ck-' + role + '-bg:' + b.bg);
|
|
825
|
+
d.push('--ck-' + role + '-fg:' + b.fg);
|
|
826
|
+
d.push('--ck-' + role + '-bd:' + b.border);
|
|
827
|
+
d.push('--ck-' + role + '-bw:' + b.borderWidth + 'px');
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (extra) {
|
|
831
|
+
for (var k = 0; k < extra.length; k++) d.push(extra[k]);
|
|
832
|
+
}
|
|
480
833
|
return sel + '{' + d.join(';') + '}';
|
|
481
834
|
}
|
|
482
835
|
|
|
@@ -498,11 +851,9 @@
|
|
|
498
851
|
function buildThemeCss(cfg) {
|
|
499
852
|
var theme = (cfg && cfg.theme) || {};
|
|
500
853
|
var dk = (theme.dark && typeof theme.dark === 'object') ? theme.dark : {};
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
// palette rather than a half-substituted one.
|
|
505
|
-
var radius = sanitizeCssValue('length', theme.radius, LIGHT_RADIUS);
|
|
854
|
+
|
|
855
|
+
var radius = resolveRadius(theme);
|
|
856
|
+
var font = resolveFont(theme);
|
|
506
857
|
|
|
507
858
|
// Light: config overrides on top of the built-in light palette.
|
|
508
859
|
var light = derive(LIGHT,
|
|
@@ -512,17 +863,37 @@
|
|
|
512
863
|
|
|
513
864
|
// Dark: theme.dark overrides on top of the built-in dark palette.
|
|
514
865
|
// A light-only theme.accent deliberately does NOT carry into dark — the
|
|
515
|
-
// default #2B50D8 on #
|
|
866
|
+
// default #2B50D8 on #1c1c1e is ~2.4:1 and would fail AA.
|
|
516
867
|
var dark = derive(DARK,
|
|
517
868
|
sanitizeCssValue('color', dk.bg, DARK.bg),
|
|
518
869
|
sanitizeCssValue('color', dk.ink, DARK.ink));
|
|
519
870
|
if (str(dk.accent)) dark.accent = sanitizeCssValue('color', dk.accent, DARK.accent);
|
|
520
871
|
if (str(dk.onAccent)) dark.onAccent = sanitizeCssValue('color', dk.onAccent, DARK.onAccent);
|
|
521
872
|
|
|
873
|
+
// Buttons are resolved per mode against that mode's real card colour, so a
|
|
874
|
+
// custom dark card changes the outline border the same way a custom light
|
|
875
|
+
// one does.
|
|
876
|
+
var lightBtn = resolveButtonStyles(theme, 'light', light);
|
|
877
|
+
var darkBtn = resolveButtonStyles(theme, 'dark', dark);
|
|
878
|
+
light.__buttons = lightBtn.buttons;
|
|
879
|
+
dark.__buttons = darkBtn.buttons;
|
|
880
|
+
light.__link = lightBtn.link;
|
|
881
|
+
dark.__link = darkBtn.link;
|
|
882
|
+
|
|
522
883
|
var mode = theme.mode;
|
|
523
884
|
if (mode !== 'light' && mode !== 'dark') mode = 'auto';
|
|
524
885
|
|
|
525
|
-
|
|
886
|
+
// Geometry and family are mode-independent, so they ride on the base block.
|
|
887
|
+
var rootExtra = [
|
|
888
|
+
'--ck-radius-card:' + radius.card + 'px',
|
|
889
|
+
'--ck-radius-btn:' + radius.button + 'px',
|
|
890
|
+
// Kept as an alias so any integrator CSS (and older builds' rules) that
|
|
891
|
+
// reads --ck-radius still lands on the card radius.
|
|
892
|
+
'--ck-radius:' + radius.card + 'px',
|
|
893
|
+
'--ck-font:' + font
|
|
894
|
+
];
|
|
895
|
+
|
|
896
|
+
var out = [tokenBlock(':host', light, rootExtra)];
|
|
526
897
|
if (mode === 'auto') {
|
|
527
898
|
// forced-light class must still beat a dark system preference
|
|
528
899
|
out.push('@media (prefers-color-scheme: dark){' +
|
|
@@ -541,7 +912,8 @@
|
|
|
541
912
|
':host(.ck-mode-dark){--ck-muted:' + DARK.muted + ';--ck-line:' + DARK.line +
|
|
542
913
|
';--ck-soft:' + DARK.soft + '}}');
|
|
543
914
|
|
|
544
|
-
return { css: out.join('\n'), mode: mode
|
|
915
|
+
return { css: out.join('\n'), mode: mode, radius: radius, font: font,
|
|
916
|
+
light: lightBtn, dark: darkBtn };
|
|
545
917
|
}
|
|
546
918
|
|
|
547
919
|
function applyTheme(cfg) {
|
|
@@ -642,6 +1014,33 @@
|
|
|
642
1014
|
return wrap;
|
|
643
1015
|
}
|
|
644
1016
|
|
|
1017
|
+
/* SPEC V1.6 §2 — what «Подробнее» does.
|
|
1018
|
+
|
|
1019
|
+
Before 0.5.0 the link was rendered with href="#" and NO click handler: it
|
|
1020
|
+
did nothing but jump to the top of the page. There are now three honest
|
|
1021
|
+
outcomes, and the default depends on whether a policy address exists —
|
|
1022
|
+
'settings' without one, 'policy' with one, so an integrator who only
|
|
1023
|
+
supplies a URL gets the link they obviously meant. This is why
|
|
1024
|
+
detailsAction must NOT carry a default in the core's DEFAULT_CONFIG: a
|
|
1025
|
+
merged concrete value would make the URL-sensitive default unreachable.
|
|
1026
|
+
|
|
1027
|
+
'policy' with no usable URL degrades to 'settings' rather than rendering a
|
|
1028
|
+
dead link. Only http(s) is accepted — javascript: and data: URLs in a
|
|
1029
|
+
link the visitor is invited to click are an XSS vector, and a relative
|
|
1030
|
+
path cannot be validated here without a base. */
|
|
1031
|
+
function resolveDetails(cfg) {
|
|
1032
|
+
var texts = (cfg && cfg.texts && typeof cfg.texts === 'object') ? cfg.texts : {};
|
|
1033
|
+
var url = str(texts.policyUrl);
|
|
1034
|
+
if (url && !/^https?:\/\//i.test(url)) url = null;
|
|
1035
|
+
|
|
1036
|
+
var action = texts.detailsAction;
|
|
1037
|
+
if (action !== 'policy' && action !== 'settings' && action !== 'hide') {
|
|
1038
|
+
action = url ? 'policy' : 'settings';
|
|
1039
|
+
}
|
|
1040
|
+
if (action === 'policy' && !url) action = 'settings';
|
|
1041
|
+
return { kind: action, href: action === 'policy' ? url : null };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
645
1044
|
// Unknown type -> bar/bottom. Known type with an unrecognized position ->
|
|
646
1045
|
// that type's own default (bar: bottom, box: bottom-left — the side away
|
|
647
1046
|
// from the chat widgets and scroll-to-top buttons most sites put on the right).
|
|
@@ -676,10 +1075,26 @@
|
|
|
676
1075
|
body.appendChild(h);
|
|
677
1076
|
|
|
678
1077
|
var p = el('p');
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
1078
|
+
var det = resolveDetails(cfg);
|
|
1079
|
+
if (det.kind === 'hide') {
|
|
1080
|
+
p.appendChild(document.createTextNode(T.bannerText));
|
|
1081
|
+
} else {
|
|
1082
|
+
p.appendChild(document.createTextNode(T.bannerText + ' '));
|
|
1083
|
+
if (det.kind === 'policy') {
|
|
1084
|
+
var link = el('a', 'ck-banner__more', T.more);
|
|
1085
|
+
link.href = det.href;
|
|
1086
|
+
link.target = '_blank';
|
|
1087
|
+
link.rel = 'noopener'; // never hand the policy page window.opener
|
|
1088
|
+
p.appendChild(link);
|
|
1089
|
+
} else {
|
|
1090
|
+
// A control that changes what is on screen is a button, not a link:
|
|
1091
|
+
// screen readers announce it correctly and it needs no href to fake.
|
|
1092
|
+
var more = el('button', 'ck-banner__more ck-linkbtn', T.more);
|
|
1093
|
+
more.type = 'button';
|
|
1094
|
+
more.addEventListener('click', function () { openPanel(more); });
|
|
1095
|
+
p.appendChild(more);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
683
1098
|
body.appendChild(p);
|
|
684
1099
|
|
|
685
1100
|
var pb = buildPoweredBy(cfg);
|
|
@@ -701,11 +1116,11 @@
|
|
|
701
1116
|
b.appendChild(body);
|
|
702
1117
|
|
|
703
1118
|
var actions = el('div', 'ck-actions');
|
|
704
|
-
var accept = el('button', 'ck-btn ck-btn--
|
|
1119
|
+
var accept = el('button', 'ck-btn ck-btn--accept', T.acceptAll);
|
|
705
1120
|
accept.type = 'button';
|
|
706
|
-
var reject = el('button', 'ck-btn ck-btn--
|
|
1121
|
+
var reject = el('button', 'ck-btn ck-btn--reject', T.rejectAll);
|
|
707
1122
|
reject.type = 'button';
|
|
708
|
-
var custom = el('button', 'ck-btn ck-btn--
|
|
1123
|
+
var custom = el('button', 'ck-btn ck-btn--settings', T.customize);
|
|
709
1124
|
custom.type = 'button';
|
|
710
1125
|
|
|
711
1126
|
accept.addEventListener('click', function () { doAcceptAll(); });
|
|
@@ -940,6 +1355,10 @@
|
|
|
940
1355
|
String(c.language || 'auto'),
|
|
941
1356
|
lay.type, String(lay.position),
|
|
942
1357
|
Array.isArray(table) ? table.length : 0,
|
|
1358
|
+
// Structural: link / button / nothing are three different DOM shapes,
|
|
1359
|
+
// and mount() is one-shot. Buttons and colours are deliberately NOT
|
|
1360
|
+
// here — they are token values and restyle in place.
|
|
1361
|
+
resolveDetails(c).kind,
|
|
943
1362
|
brandSignature(c)
|
|
944
1363
|
].join('|');
|
|
945
1364
|
}
|
|
@@ -996,6 +1415,33 @@
|
|
|
996
1415
|
|
|
997
1416
|
/* ---------------------------------------------------------------- events */
|
|
998
1417
|
|
|
1418
|
+
/* ------------------------------------------------------- pure exports */
|
|
1419
|
+
|
|
1420
|
+
/* SPEC V1.6 §1: the cabinet's theme editor must show the same numbers the
|
|
1421
|
+
banner actually paints — «один код — одни числа». Published BEFORE the
|
|
1422
|
+
SSR guard below on purpose: these are pure functions with no DOM in them,
|
|
1423
|
+
so they are reachable (and testable in node) on a page or in a process
|
|
1424
|
+
that never renders anything.
|
|
1425
|
+
|
|
1426
|
+
Not a stable public API — the underscore says so — but the theme editor
|
|
1427
|
+
and the debug panel are both expected to read it rather than reimplement
|
|
1428
|
+
the arithmetic and drift. */
|
|
1429
|
+
(function () {
|
|
1430
|
+
var ck = api();
|
|
1431
|
+
if (!ck) return;
|
|
1432
|
+
ck._contrast = {
|
|
1433
|
+
relativeLuminance: relativeLuminance,
|
|
1434
|
+
contrastRatio: contrastRatio,
|
|
1435
|
+
ensureContrast: ensureContrast,
|
|
1436
|
+
stepToContrast: stepToContrast,
|
|
1437
|
+
resolveButtonStyles: resolveButtonStyles,
|
|
1438
|
+
resolveRadius: resolveRadius,
|
|
1439
|
+
resolveFont: resolveFont,
|
|
1440
|
+
resolveDetails: resolveDetails,
|
|
1441
|
+
buildThemeCss: buildThemeCss
|
|
1442
|
+
};
|
|
1443
|
+
})();
|
|
1444
|
+
|
|
999
1445
|
// SSR-safe: with no DOM there is nothing to render or listen to, so importing
|
|
1000
1446
|
// this file in Node is a no-op rather than a throw (mirrors the core).
|
|
1001
1447
|
if (typeof document === 'undefined') return;
|