@schukai/monster 3.117.3 → 3.118.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/CHANGELOG.md
CHANGED
package/package.json
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"author":"schukai GmbH","dependencies":{"@floating-ui/dom":"^1.6.13","@popperjs/core":"^2.11.8","buffer":"^6.0.3"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"3.
|
1
|
+
{"author":"schukai GmbH","dependencies":{"@floating-ui/dom":"^1.6.13","@popperjs/core":"^2.11.8","buffer":"^6.0.3"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"3.118.0"}
|
package/source/types/version.mjs
CHANGED
@@ -0,0 +1,304 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
|
3
|
+
* Node module: @schukai/monster
|
4
|
+
*
|
5
|
+
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
|
6
|
+
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
|
7
|
+
*
|
8
|
+
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
|
9
|
+
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
|
10
|
+
* For more information about purchasing a commercial license, please contact schukai GmbH.
|
11
|
+
*
|
12
|
+
* SPDX-License-Identifier: AGPL-3.0
|
13
|
+
*/
|
14
|
+
|
15
|
+
/**
|
16
|
+
* accessibleColor
|
17
|
+
*
|
18
|
+
* `accessibleColor(baseColor [, minContrast])` returns a second
|
19
|
+
* color that meets the given WCAG contrast-ratio against
|
20
|
+
* `baseColor`.
|
21
|
+
*
|
22
|
+
* • `baseColor` String – 3- or 6-digit HEX with or without “#”.
|
23
|
+
* • `minContrast` Number – target ratio (default **4.5** for WCAG-AA).
|
24
|
+
* • Return value String – always a 6-digit HEX “#rrggbb”.
|
25
|
+
*
|
26
|
+
* HOW IT WORKS
|
27
|
+
* ------------
|
28
|
+
* 1. Fast path: tries pure **white** and **black**.
|
29
|
+
* 2. If both fail, keeps the original hue/saturation and shifts
|
30
|
+
* lightness in 5 % steps until the ratio is reached.
|
31
|
+
* 3. Last resort: picks the better of white / black if neither
|
32
|
+
* reaches the exact ratio after 100 %.
|
33
|
+
*
|
34
|
+
* TYPICAL USE-CASES
|
35
|
+
* -----------------
|
36
|
+
* • **Dynamic theming**: users pick a brand color → generate safe
|
37
|
+
* text/background automatically.
|
38
|
+
* • **Badges / chips**: text on colored pills that must stay legible.
|
39
|
+
* • **Charts**: ensure labels on bars/slices have enough contrast.
|
40
|
+
* • **Map markers**: pick a halo color that keeps icons readable.
|
41
|
+
* • **Data tables**: alternating row colors that adapt to theme.
|
42
|
+
*
|
43
|
+
* BASIC EXAMPLES
|
44
|
+
* --------------
|
45
|
+
* // Default AA (4.5 : 1)
|
46
|
+
* const badgeFg = '#007bff'; // brand blue
|
47
|
+
* const badgeBg = accessibleColor(badgeFg); // → '#ffffff'
|
48
|
+
*
|
49
|
+
* // Pick a background for an arbitrary text color
|
50
|
+
* const text = '#e52929';
|
51
|
+
* const bg = accessibleColor(text); // may return '#ffffff'
|
52
|
+
*
|
53
|
+
* // Request AAA (7 : 1) for small text
|
54
|
+
* const hiVis = accessibleColor('#888888', 7); // → '#000000'
|
55
|
+
*
|
56
|
+
* EXTENDED EXAMPLES
|
57
|
+
* -----------------
|
58
|
+
* // 1) Generate theme object
|
59
|
+
* const base = '#1abc9c';
|
60
|
+
* const theme = {
|
61
|
+
* primary: base,
|
62
|
+
* onPrimary: accessibleColor(base), // safe text color
|
63
|
+
* border: accessibleColor(base, 3), // borders need only 3 : 1
|
64
|
+
* };
|
65
|
+
*
|
66
|
+
* // 2) Build badge component
|
67
|
+
* function Badge({ color, children }) {
|
68
|
+
* const background = color;
|
69
|
+
* const foreground = accessibleColor(color); // text or icon
|
70
|
+
* return `<span style="
|
71
|
+
* background:${background};
|
72
|
+
* color:${foreground};
|
73
|
+
* padding:0.25em 0.5em;
|
74
|
+
* border-radius:0.25rem;">${children}</span>`;
|
75
|
+
* }
|
76
|
+
*
|
77
|
+
* // 3) Ensure chart label contrast
|
78
|
+
* const segmentColor = '#ffcc00';
|
79
|
+
* const labelColor = accessibleColor(segmentColor, 3); // 3 : 1 OK for large labels
|
80
|
+
*
|
81
|
+
* NOTES & TIPS
|
82
|
+
* ------------
|
83
|
+
* • `minContrast = 3` is allowed for:
|
84
|
+
* – Large / bold text (≥ 24 px normal or ≥ 19 px bold).
|
85
|
+
* – Non-text UI parts (icons, borders).
|
86
|
+
* • The algorithm preserves hue & saturation whenever possible,
|
87
|
+
* so resulting colors match the original palette.
|
88
|
+
* • Performance: pure JS, no DOM required – safe in workers.
|
89
|
+
* • Edge-case: middle-gray “#777777” – function will return black
|
90
|
+
* because white misses 4.5 : 1 by a hair (~4.3 : 1).
|
91
|
+
*
|
92
|
+
* @param color
|
93
|
+
* @param minContrast
|
94
|
+
* @returns {string|string}
|
95
|
+
* @summary This function generates a color that meets the WCAG contrast ratio requirements against a given base color.
|
96
|
+
*/
|
97
|
+
export function accessibleColor(color, minContrast = 4.5) {
|
98
|
+
const baseColor = toHex(color);
|
99
|
+
const baseHex = normalizeHex(baseColor);
|
100
|
+
|
101
|
+
const white = "#ffffff";
|
102
|
+
const black = "#000000";
|
103
|
+
|
104
|
+
const baseLum = luminance(baseHex);
|
105
|
+
|
106
|
+
// 1) Quick check: try pure white or pure black first
|
107
|
+
if (contrastRatio(baseLum, luminance(white)) >= minContrast) return white;
|
108
|
+
if (contrastRatio(baseLum, luminance(black)) >= minContrast) return black;
|
109
|
+
|
110
|
+
// 2) Fallback: keep hue/saturation but shift lightness until contrast is OK
|
111
|
+
let { h, s, l } = hexToHsl(baseHex);
|
112
|
+
const direction = l < 0.5 ? +1 : -1; // brighten or darken?
|
113
|
+
|
114
|
+
for (let i = 1; i <= 20; i++) {
|
115
|
+
// max 20 × 5 % = 100 %
|
116
|
+
l = clamp(l + direction * 0.05, 0, 1);
|
117
|
+
const candidate = hslToHex({ h, s, l });
|
118
|
+
if (contrastRatio(luminance(candidate), baseLum) >= minContrast) {
|
119
|
+
return candidate;
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
// 3) Last resort: pick the better of (almost-good) white/black
|
124
|
+
return contrastRatio(baseLum, luminance(white)) >
|
125
|
+
contrastRatio(baseLum, luminance(black))
|
126
|
+
? white
|
127
|
+
: black;
|
128
|
+
}
|
129
|
+
|
130
|
+
/**
|
131
|
+
* Normalizes a given hexadecimal color code to ensure it is in the standard 6-character and lowercase format, prefixed with a '#'.
|
132
|
+
*
|
133
|
+
* @param {string} hex - The hexadecimal color code, which can be in 3-character or 6-character format, with or without the leading '#'.
|
134
|
+
* @return {string} - The normalized 6-character hexadecimal color code in lowercase, prefixed with a '#'.
|
135
|
+
*/
|
136
|
+
function normalizeHex(hex) {
|
137
|
+
hex = hex.toString().replace(/^#/, "");
|
138
|
+
if (hex.length === 3)
|
139
|
+
hex = hex
|
140
|
+
.split("")
|
141
|
+
.map((c) => c + c)
|
142
|
+
.join("");
|
143
|
+
return "#" + hex.toLowerCase();
|
144
|
+
}
|
145
|
+
|
146
|
+
/**
|
147
|
+
* Converts a hexadecimal color code to an RGB object.
|
148
|
+
*
|
149
|
+
* @param {string} hex - A hexadecimal color string (e.g., "#ff0000" or "ff0000").
|
150
|
+
* @return {Object} An object representing the RGB color with properties:
|
151
|
+
* - r: The red component (0-255).
|
152
|
+
* - g: The green component (0-255).
|
153
|
+
* - b: The blue component (0-255).
|
154
|
+
*/
|
155
|
+
function hexToRgb(hex) {
|
156
|
+
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
157
|
+
return {
|
158
|
+
r: parseInt(m[1], 16),
|
159
|
+
g: parseInt(m[2], 16),
|
160
|
+
b: parseInt(m[3], 16),
|
161
|
+
};
|
162
|
+
}
|
163
|
+
|
164
|
+
/**
|
165
|
+
* Calculates the relative luminance of a color based on its hexadecimal representation.
|
166
|
+
* The luminance is calculated using the standard formula for relative luminance
|
167
|
+
* in the sRGB color space.
|
168
|
+
*
|
169
|
+
* @param {string} hex - The hexadecimal color code (e.g., "#FFFFFF" or "#000000").
|
170
|
+
* @return {number} The relative luminance value, ranging from 0 (darkest) to 1 (lightest).
|
171
|
+
*/
|
172
|
+
function luminance(hex) {
|
173
|
+
const { r, g, b } = hexToRgb(hex);
|
174
|
+
const srgb = [r, g, b]
|
175
|
+
.map((v) => v / 255)
|
176
|
+
.map((v) =>
|
177
|
+
v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4),
|
178
|
+
);
|
179
|
+
return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2];
|
180
|
+
}
|
181
|
+
|
182
|
+
/**
|
183
|
+
* Calculates the contrast ratio between two luminance values.
|
184
|
+
*
|
185
|
+
* The contrast ratio is determined using a formula defined by the Web Content Accessibility Guidelines (WCAG).
|
186
|
+
* This ratio helps evaluate the readability of text or visual elements against a background.
|
187
|
+
*
|
188
|
+
* @param {number} l1 - The relative luminance of the lighter element.
|
189
|
+
* @param {number} l2 - The relative luminance of the darker element.
|
190
|
+
* @returns {number} The contrast ratio, a value ranging from 1 to 21.
|
191
|
+
*/
|
192
|
+
const contrastRatio = (l1, l2) =>
|
193
|
+
(Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
194
|
+
|
195
|
+
/**
|
196
|
+
* Converts RGB color values to HSL color space.
|
197
|
+
*
|
198
|
+
* @param {Object} rgb - The RGB color object.
|
199
|
+
* @param {number} rgb.r - The red component (0-255).
|
200
|
+
* @param {number} rgb.g - The green component (0-255).
|
201
|
+
* @param {number} rgb.b - The blue component (0-255).
|
202
|
+
* @return {Object} An object with the HSL values.
|
203
|
+
* @return {number} return.h - The hue component (0-1).
|
204
|
+
* @return {number} return.s - The saturation component (0-1).
|
205
|
+
* @return {number} return.l - The lightness component (0-1).
|
206
|
+
*/
|
207
|
+
function rgbToHsl({ r, g, b }) {
|
208
|
+
r /= 255;
|
209
|
+
g /= 255;
|
210
|
+
b /= 255;
|
211
|
+
const max = Math.max(r, g, b),
|
212
|
+
min = Math.min(r, g, b);
|
213
|
+
let h,
|
214
|
+
s,
|
215
|
+
l = (max + min) / 2;
|
216
|
+
|
217
|
+
if (max === min) {
|
218
|
+
h = s = 0;
|
219
|
+
} else {
|
220
|
+
const d = max - min;
|
221
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
222
|
+
switch (max) {
|
223
|
+
case r:
|
224
|
+
h = (g - b) / d + (g < b ? 6 : 0);
|
225
|
+
break;
|
226
|
+
case g:
|
227
|
+
h = (b - r) / d + 2;
|
228
|
+
break;
|
229
|
+
case b:
|
230
|
+
h = (r - g) / d + 4;
|
231
|
+
break;
|
232
|
+
}
|
233
|
+
h /= 6;
|
234
|
+
}
|
235
|
+
return { h, s, l };
|
236
|
+
}
|
237
|
+
|
238
|
+
/**
|
239
|
+
* Converts an HSL color value to its hexadecimal representation.
|
240
|
+
*
|
241
|
+
* @param {Object} hsl - An object representing the HSL color.
|
242
|
+
* @param {number} hsl.h - Hue, a number between 0 and 1.
|
243
|
+
* @param {number} hsl.s - Saturation, a number between 0 and 1.
|
244
|
+
* @param {number} hsl.l - Lightness, a number between 0 and 1.
|
245
|
+
* @return {string} The hexadecimal color string in the format "#RRGGBB".
|
246
|
+
*/
|
247
|
+
function hslToHex({ h, s, l }) {
|
248
|
+
const hue2rgb = (p, q, t) => {
|
249
|
+
if (t < 0) t += 1;
|
250
|
+
if (t > 1) t -= 1;
|
251
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
252
|
+
if (t < 1 / 2) return q;
|
253
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
254
|
+
return p;
|
255
|
+
};
|
256
|
+
let r, g, b;
|
257
|
+
if (s === 0) {
|
258
|
+
r = g = b = l;
|
259
|
+
} // achromatic
|
260
|
+
else {
|
261
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
262
|
+
const p = 2 * l - q;
|
263
|
+
r = hue2rgb(p, q, h + 1 / 3);
|
264
|
+
g = hue2rgb(p, q, h);
|
265
|
+
b = hue2rgb(p, q, h - 1 / 3);
|
266
|
+
}
|
267
|
+
const toHex = (x) => ("0" + Math.round(x * 255).toString(16)).slice(-2);
|
268
|
+
return "#" + toHex(r) + toHex(g) + toHex(b);
|
269
|
+
}
|
270
|
+
|
271
|
+
// Keep number between 0 and 1
|
272
|
+
const clamp = (val, min, max) => Math.min(max, Math.max(min, val));
|
273
|
+
|
274
|
+
/**
|
275
|
+
* Converts any CSS color to full 6-digit HEX "#rrggbb".
|
276
|
+
* Falls back to throwing an Error if the input is not a valid color.
|
277
|
+
*
|
278
|
+
* Works in all browsers (DOM required). For Node.js use a library like
|
279
|
+
* `color` or `tinycolor2`, or adapt the parser accordingly.
|
280
|
+
*/
|
281
|
+
function toHex(color) {
|
282
|
+
// Use the browser to parse the color for us
|
283
|
+
const temp = new Option().style; // <option> exists in all browsers
|
284
|
+
temp.color = color;
|
285
|
+
|
286
|
+
// Invalid → empty string
|
287
|
+
if (!temp.color) {
|
288
|
+
throw new Error(`"${color}" is not a valid CSS color`);
|
289
|
+
}
|
290
|
+
|
291
|
+
// temp.color is now normalized, e.g. "rgb(255, 0, 0)" or "rgba(0, 0, 0, 0.5)"
|
292
|
+
const m = temp.color.match(/\d+/g).map(Number); // [r, g, b, (a)]
|
293
|
+
const [r, g, b] = m;
|
294
|
+
|
295
|
+
// If alpha channel < 1 the color is translucent; composite onto white
|
296
|
+
const a = m[3] !== undefined ? m[3] / 255 || 0 : 1;
|
297
|
+
const toWhite = (c) => Math.round(a * c + (1 - a) * 255);
|
298
|
+
|
299
|
+
const rr = (a < 1 ? toWhite(r) : r).toString(16).padStart(2, "0");
|
300
|
+
const gg = (a < 1 ? toWhite(g) : g).toString(16).padStart(2, "0");
|
301
|
+
const bb = (a < 1 ? toWhite(b) : b).toString(16).padStart(2, "0");
|
302
|
+
|
303
|
+
return `#${rr}${gg}${bb}`;
|
304
|
+
}
|
package/test/cases/monster.mjs
CHANGED
package/test/web/test.html
CHANGED
@@ -9,8 +9,8 @@
|
|
9
9
|
</head>
|
10
10
|
<body>
|
11
11
|
<div id="headline" style="display: flex;align-items: center;justify-content: center;flex-direction: column;">
|
12
|
-
<h1 style='margin-bottom: 0.1em;'>Monster 3.117.
|
13
|
-
<div id="lastupdate" style='font-size:0.7em'>last update
|
12
|
+
<h1 style='margin-bottom: 0.1em;'>Monster 3.117.3</h1>
|
13
|
+
<div id="lastupdate" style='font-size:0.7em'>last update Do 1. Mai 12:16:41 CEST 2025</div>
|
14
14
|
</div>
|
15
15
|
<div id="mocha-errors"
|
16
16
|
style="color: red;font-weight: bold;display: flex;align-items: center;justify-content: center;flex-direction: column;margin:20px;"></div>
|