tiny-essentials 1.13.2 → 1.14.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.
@@ -0,0 +1,408 @@
1
+ /******/ (() => { // webpackBootstrap
2
+ /******/ "use strict";
3
+ /******/ // The require scope
4
+ /******/ var __webpack_require__ = {};
5
+ /******/
6
+ /************************************************************************/
7
+ /******/ /* webpack/runtime/define property getters */
8
+ /******/ (() => {
9
+ /******/ // define getter functions for harmony exports
10
+ /******/ __webpack_require__.d = (exports, definition) => {
11
+ /******/ for(var key in definition) {
12
+ /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
13
+ /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
14
+ /******/ }
15
+ /******/ }
16
+ /******/ };
17
+ /******/ })();
18
+ /******/
19
+ /******/ /* webpack/runtime/hasOwnProperty shorthand */
20
+ /******/ (() => {
21
+ /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
22
+ /******/ })();
23
+ /******/
24
+ /************************************************************************/
25
+ var __webpack_exports__ = {};
26
+
27
+ // EXPORTS
28
+ __webpack_require__.d(__webpack_exports__, {
29
+ TinyNotifications: () => (/* reexport */ libs_TinyNotifications)
30
+ });
31
+
32
+ ;// ./src/v1/basics/text.mjs
33
+ /**
34
+ * Converts a string to title case where the first letter of each word is capitalized.
35
+ * All other letters are converted to lowercase.
36
+ *
37
+ * Example: "hello world" -> "Hello World"
38
+ *
39
+ * @param {string} str - The string to be converted to title case.
40
+ * @returns {string} The string converted to title case.
41
+ */
42
+ function toTitleCase(str) {
43
+ return str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
44
+ }
45
+
46
+ /**
47
+ * Converts a string to title case where the first letter of each word is capitalized,
48
+ * but the first letter of the entire string is left lowercase.
49
+ *
50
+ * Example: "hello world" -> "hello World"
51
+ *
52
+ * @param {string} str - The string to be converted to title case with the first letter in lowercase.
53
+ * @returns {string} The string converted to title case with the first letter in lowercase.
54
+ */
55
+ function toTitleCaseLowerFirst(str) {
56
+ const titleCased = str.replace(
57
+ /\w\S*/g,
58
+ (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(),
59
+ );
60
+ return titleCased.charAt(0).toLowerCase() + titleCased.slice(1);
61
+ }
62
+
63
+ /**
64
+ * Enables a keyboard shortcut to toggle a CSS class on the document body.
65
+ *
66
+ * This function listens for a specific key combination: `Ctrl + Alt + [key]`.
67
+ * When triggered, it prevents the default behavior and toggles the
68
+ * `detect-made-by-ai` class on the `<body>`, which can be used to apply visual
69
+ * indicators or filters on AI-generated content.
70
+ *
71
+ * If executed outside of a browser environment (e.g., in Node.js), the function logs an error and exits.
72
+ * If the `<body>` is not available at the moment the shortcut is triggered, a warning is logged.
73
+ *
74
+ * @param {string} [key='a'] - The lowercase character key to be used in combination with Ctrl and Alt.
75
+ */
76
+ function addAiMarkerShortcut(key = 'a') {
77
+ if (typeof HTMLElement === 'undefined') {
78
+ console.error(
79
+ '[AiMarkerShortcut] Environment does not support the DOM. This function must be run in a browser.',
80
+ );
81
+ return;
82
+ }
83
+ document.addEventListener('keydown', function (event) {
84
+ if (event.ctrlKey && event.altKey && event.key.toLowerCase() === key) {
85
+ event.preventDefault(); // Prevent any default behavior
86
+ if (!document.body) {
87
+ console.warn(
88
+ '[AiMarkerShortcut] <body> element not found. Cannot toggle class. Ensure the DOM is fully loaded when using the shortcut.',
89
+ );
90
+ return;
91
+ }
92
+ document.body.classList.toggle('detect-made-by-ai');
93
+ }
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Trims a text string to a specified character limit, attempting to avoid cutting words in half.
99
+ * If a space is found before the limit and it's not too far from the limit (at least 60%),
100
+ * the cut is made at that space; otherwise, the text is hard-cut at the limit.
101
+ * If the input is shorter than the limit, it is returned unchanged.
102
+ *
103
+ * @param {string} text - The input text to be trimmed.
104
+ * @param {number} limit - The maximum number of characters allowed.
105
+ * @param {number} [safeCutZone=0.6] - A decimal between 0 and 1 representing the minimal acceptable position
106
+ * (as a fraction of `limit`) to cut at a space. Defaults to 0.6.
107
+ * @returns {string} - The trimmed text, possibly ending with an ellipsis ("...").
108
+ * @throws {TypeError} - Throws if `text` is not a string.
109
+ * @throws {TypeError} - Throws if `limit` is not a positive integer.
110
+ * @throws {TypeError} - Throws if `safeCutZone` is not a number between 0 and 1 (inclusive).
111
+ */
112
+ function safeTextTrim(text, limit, safeCutZone = 0.6) {
113
+ if (typeof text !== 'string')
114
+ throw new TypeError(`Expected a string for 'text', but received ${typeof text}`);
115
+ if (!Number.isInteger(limit) || limit <= 0)
116
+ throw new TypeError(`Expected 'limit' to be a positive integer, but received ${limit}`);
117
+ if (typeof safeCutZone !== 'number' || safeCutZone < 0 || safeCutZone > 1)
118
+ throw new TypeError(
119
+ `Expected 'safeCutZone' to be a number between 0 and 1, but received ${safeCutZone}`,
120
+ );
121
+
122
+ let result = text.trim();
123
+ if (result.length > limit) {
124
+ // Try to cut the string into a space before the limit
125
+ const safeCut = result.lastIndexOf(' ', limit);
126
+
127
+ if (safeCut > 0 && safeCut >= limit * safeCutZone) {
128
+ // Only cuts where there is a space, and if the cut is not too early
129
+ return `${result.substring(0, safeCut).trim()}...`;
130
+ } else {
131
+ // Emergency: Cuts straight to the limit and adds "...".
132
+ return `${result.substring(0, limit).trim()}...`;
133
+ }
134
+ }
135
+
136
+ return result;
137
+ }
138
+
139
+ /*
140
+ import { useEffect } from "react";
141
+
142
+ function KeyPressHandler() {
143
+ useEffect(() => {
144
+ const handleKeyDown = (event) => {
145
+ if (event.ctrlKey && event.altKey && event.key.toLowerCase() === "a") {
146
+ event.preventDefault();
147
+ document.body.classList.toggle("detect-made-by-ai");
148
+ }
149
+ };
150
+
151
+ document.addEventListener("keydown", handleKeyDown);
152
+ return () => {
153
+ document.removeEventListener("keydown", handleKeyDown);
154
+ };
155
+ }, []);
156
+
157
+ return null;
158
+ }
159
+
160
+ export default KeyPressHandler;
161
+ */
162
+
163
+ ;// ./src/v1/libs/TinyNotifications.mjs
164
+
165
+
166
+ /**
167
+ * A utility class to manage browser notifications with sound and custom behavior.
168
+ * Useful for triggering system notifications with optional sound, avatar icon, body truncation, and click actions.
169
+ *
170
+ * @class
171
+ */
172
+ class TinyNotifications {
173
+ /** @type {boolean} Whether notifications are currently allowed by the user. */
174
+ #allowed = false;
175
+
176
+ /** @type {boolean} Indicates whether the user has already requested permission at least once. */
177
+ #permissionRequested = false;
178
+
179
+ /** @type {HTMLAudioElement|null} Audio element to play when a notification is triggered. */
180
+ #audio = null;
181
+
182
+ /** @type {number} Maximum number of characters in the notification body. */
183
+ #bodyLimit = 100;
184
+
185
+ /** @type {string|null} Default avatar icon URL for notifications. */
186
+ #defaultIcon = null;
187
+
188
+ /** @type {(this: Notification, evt: Event) => any} Default handler when a notification is clicked. */
189
+ #defaultOnClick;
190
+
191
+ /**
192
+ * Constructs a new instance of TinyNotifications.
193
+ *
194
+ * @param {Object} [settings={}] - Optional settings to initialize the notification manager.
195
+ * @param {string|HTMLAudioElement|null} [settings.audio] - Path or URL to the audio file for notification sounds.
196
+ * @param {string|null} [settings.defaultIcon] - Default icon URL to be used in notifications.
197
+ * @param {number} [settings.bodyLimit=100] - Maximum number of characters allowed in the notification body.
198
+ * @param {(this: Notification, evt: Event) => any} [settings.defaultOnClick] - Default function to execute when a notification is clicked.
199
+ * @throws {TypeError} If any of the parameters are of an invalid type.
200
+ */
201
+ constructor({
202
+ audio = null,
203
+ defaultIcon = null,
204
+ bodyLimit = 100,
205
+ defaultOnClick = function (event) {
206
+ event.preventDefault();
207
+ if (window.focus) window.focus();
208
+ this.close();
209
+ },
210
+ } = {}) {
211
+ if (!(audio instanceof HTMLAudioElement) && typeof audio !== 'string' && audio !== null)
212
+ throw new TypeError('audio must be an instance of HTMLAudioElement or null.');
213
+ if (defaultIcon !== null && typeof defaultIcon !== 'string')
214
+ throw new TypeError('defaultIcon must be a string or null.');
215
+ if (!Number.isFinite(bodyLimit) || bodyLimit < 0)
216
+ throw new TypeError('bodyLimit must be a non-negative number.');
217
+ if (typeof defaultOnClick !== 'function')
218
+ throw new TypeError('defaultOnClick must be a function.');
219
+
220
+ this.#audio = typeof audio !== 'string' ? audio : new Audio(audio);
221
+ this.#defaultIcon = defaultIcon;
222
+ this.#bodyLimit = bodyLimit;
223
+ this.#defaultOnClick = defaultOnClick;
224
+ }
225
+
226
+ /**
227
+ * Requests permission from the user to show notifications.
228
+ * Updates the internal `#allowed` flag.
229
+ *
230
+ * @returns {Promise<boolean>} Resolves to `true` if permission is granted, otherwise `false`.
231
+ */
232
+ requestPerm() {
233
+ const tinyThis = this;
234
+ return new Promise((resolve, reject) => {
235
+ if (tinyThis.isCompatible()) {
236
+ if (Notification.permission === 'default') {
237
+ Notification.requestPermission()
238
+ .then((permission) => {
239
+ this.#permissionRequested = true;
240
+ tinyThis.#allowed = permission === 'granted';
241
+ resolve(tinyThis.#allowed);
242
+ })
243
+ .catch(reject);
244
+ } else {
245
+ this.#permissionRequested = true;
246
+ tinyThis.#allowed = Notification.permission === 'granted';
247
+ resolve(tinyThis.#allowed);
248
+ }
249
+ } else {
250
+ this.#permissionRequested = true;
251
+ tinyThis.#allowed = false;
252
+ resolve(false);
253
+ }
254
+ });
255
+ }
256
+
257
+ /**
258
+ * Checks if the Notification API is supported by the current browser.
259
+ *
260
+ * @returns {boolean} Returns `true` if notifications are supported, otherwise `false`.
261
+ */
262
+ isCompatible() {
263
+ return 'Notification' in window;
264
+ }
265
+
266
+ /**
267
+ * Sends a browser notification with the provided title and configuration.
268
+ * Truncates the body if necessary and plays a sound if configured.
269
+ *
270
+ * @param {string} title - The title of the notification.
271
+ * @param {NotificationOptions & { vibrate?: number[] }} [config={}] - Optional configuration for the notification.
272
+ * @returns {Notification|null} The created `Notification` instance, or `null` if permission is not granted.
273
+ * @throws {TypeError} If the title is not a string or config is not a valid object.
274
+ */
275
+ send(title, config = {}) {
276
+ if (!this.#permissionRequested)
277
+ throw new Error('You must call requestPerm() before sending a notification.');
278
+ if (typeof title !== 'string') throw new TypeError('title must be a string.');
279
+ if (typeof config !== 'object' || config === null)
280
+ throw new TypeError('config must be a non-null object.');
281
+
282
+ if (!this.#allowed) return null;
283
+
284
+ const { icon = this.#defaultIcon || undefined, vibrate = [200, 100, 200] } = config;
285
+ const options = { ...config };
286
+ if (typeof icon === 'string') options.icon = icon;
287
+ if (Array.isArray(vibrate)) options.vibrate = vibrate;
288
+
289
+ if (typeof options.body === 'string')
290
+ options.body = safeTextTrim(options.body, this.#bodyLimit);
291
+
292
+ const notification = new Notification(title, options);
293
+ notification.addEventListener('show', () => {
294
+ if (!(this.#audio instanceof HTMLAudioElement)) return;
295
+ this.#audio.currentTime = 0;
296
+ this.#audio.play().catch((err) => console.error(err));
297
+ });
298
+
299
+ if (typeof this.#defaultOnClick === 'function')
300
+ notification.addEventListener('click', this.#defaultOnClick);
301
+
302
+ return notification;
303
+ }
304
+
305
+ // === Getters and Setters ===
306
+
307
+ /**
308
+ * Whether the requestPerm() method was already called.
309
+ * @returns {boolean}
310
+ */
311
+ wasPermissionRequested() {
312
+ return this.#permissionRequested;
313
+ }
314
+
315
+ /**
316
+ * Returns the current permission status.
317
+ * @returns {boolean} `true` if permission was granted, otherwise `false`.
318
+ */
319
+ isAllowed() {
320
+ return this.#allowed;
321
+ }
322
+
323
+ /**
324
+ * Gets the current notification audio.
325
+ * @returns {HTMLAudioElement|null} The sound element, or `null` if not set.
326
+ */
327
+ getAudio() {
328
+ return this.#audio;
329
+ }
330
+
331
+ /**
332
+ * Sets the audio element used for notification sounds.
333
+ * @param {HTMLAudioElement|string|null} value - A valid `HTMLAudioElement` or `null` to disable sound.
334
+ * @throws {TypeError} If the value is not an `HTMLAudioElement` or `null`.
335
+ */
336
+ setAudio(value) {
337
+ if (!(value instanceof HTMLAudioElement) && typeof value !== 'string' && value !== null)
338
+ throw new TypeError('sound must be an instance of HTMLAudioElement or null.');
339
+ this.#audio = typeof value !== 'string' ? value : new Audio(value);
340
+ }
341
+
342
+ /**
343
+ * Gets the maximum length of the notification body text.
344
+ * @returns {number} Number of characters allowed.
345
+ */
346
+ getBodyLimit() {
347
+ return this.#bodyLimit;
348
+ }
349
+
350
+ /**
351
+ * Sets the maximum number of characters allowed in the notification body.
352
+ * @param {number} value - A non-negative integer.
353
+ * @throws {TypeError} If the value is not a valid non-negative number.
354
+ */
355
+ setBodyLimit(value) {
356
+ if (!Number.isFinite(value) || value < 0)
357
+ throw new TypeError('bodyLimit must be a non-negative number.');
358
+ this.#bodyLimit = value;
359
+ }
360
+
361
+ /**
362
+ * Gets the default avatar icon URL.
363
+ * @returns {string|null} The URL string or `null`.
364
+ */
365
+ getDefaultAvatar() {
366
+ return this.#defaultIcon;
367
+ }
368
+
369
+ /**
370
+ * Sets the default avatar icon URL.
371
+ * @param {string|null} value - A string URL or `null` to disable default icon.
372
+ * @throws {TypeError} If the value is not a string or `null`.
373
+ */
374
+ setDefaultAvatar(value) {
375
+ if (!(typeof value === 'string' || value === null))
376
+ throw new TypeError('defaultIcon must be a string or null.');
377
+ this.#defaultIcon = value;
378
+ }
379
+
380
+ /**
381
+ * Gets the default click event handler for notifications.
382
+ * @returns {(this: Notification, evt: Event) => any} The current click handler function.
383
+ */
384
+ getDefaultOnClick() {
385
+ return this.#defaultOnClick;
386
+ }
387
+
388
+ /**
389
+ * Sets the default click event handler for notifications.
390
+ * @param {(this: Notification, evt: Event) => any} value - A function to handle the notification click event.
391
+ * @throws {TypeError} If the value is not a function.
392
+ */
393
+ setDefaultOnClick(value) {
394
+ if (typeof value !== 'function') throw new TypeError('defaultOnClick must be a function.');
395
+ this.#defaultOnClick = value;
396
+ }
397
+ }
398
+
399
+ /* harmony default export */ const libs_TinyNotifications = (TinyNotifications);
400
+
401
+ ;// ./src/v1/build/TinyNotifications.mjs
402
+
403
+
404
+
405
+
406
+ window.TinyNotifications = __webpack_exports__.TinyNotifications;
407
+ /******/ })()
408
+ ;
@@ -0,0 +1 @@
1
+ (()=>{"use strict";var e={d:(t,i)=>{for(var n in i)e.o(i,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:i[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};e.d(t,{TinyNotifications:()=>i});const i=class{#e=!1;#t=!1;#i=null;#n=100;#o=null;#r;constructor({audio:e=null,defaultIcon:t=null,bodyLimit:i=100,defaultOnClick:n=function(e){e.preventDefault(),window.focus&&window.focus(),this.close()}}={}){if(!(e instanceof HTMLAudioElement)&&"string"!=typeof e&&null!==e)throw new TypeError("audio must be an instance of HTMLAudioElement or null.");if(null!==t&&"string"!=typeof t)throw new TypeError("defaultIcon must be a string or null.");if(!Number.isFinite(i)||i<0)throw new TypeError("bodyLimit must be a non-negative number.");if("function"!=typeof n)throw new TypeError("defaultOnClick must be a function.");this.#i="string"!=typeof e?e:new Audio(e),this.#o=t,this.#n=i,this.#r=n}requestPerm(){const e=this;return new Promise(((t,i)=>{e.isCompatible()?"default"===Notification.permission?Notification.requestPermission().then((i=>{this.#t=!0,e.#e="granted"===i,t(e.#e)})).catch(i):(this.#t=!0,e.#e="granted"===Notification.permission,t(e.#e)):(this.#t=!0,e.#e=!1,t(!1))}))}isCompatible(){return"Notification"in window}send(e,t={}){if(!this.#t)throw new Error("You must call requestPerm() before sending a notification.");if("string"!=typeof e)throw new TypeError("title must be a string.");if("object"!=typeof t||null===t)throw new TypeError("config must be a non-null object.");if(!this.#e)return null;const{icon:i=this.#o||void 0,vibrate:n=[200,100,200]}=t,o={...t};"string"==typeof i&&(o.icon=i),Array.isArray(n)&&(o.vibrate=n),"string"==typeof o.body&&(o.body=function(e,t,i=.6){if("string"!=typeof e)throw new TypeError("Expected a string for 'text', but received "+typeof e);if(!Number.isInteger(t)||t<=0)throw new TypeError(`Expected 'limit' to be a positive integer, but received ${t}`);if("number"!=typeof i||i<0||i>1)throw new TypeError(`Expected 'safeCutZone' to be a number between 0 and 1, but received ${i}`);let n=e.trim();if(n.length>t){const e=n.lastIndexOf(" ",t);return e>0&&e>=t*i?`${n.substring(0,e).trim()}...`:`${n.substring(0,t).trim()}...`}return n}(o.body,this.#n));const r=new Notification(e,o);return r.addEventListener("show",(()=>{this.#i instanceof HTMLAudioElement&&(this.#i.currentTime=0,this.#i.play().catch((e=>console.error(e))))})),"function"==typeof this.#r&&r.addEventListener("click",this.#r),r}wasPermissionRequested(){return this.#t}isAllowed(){return this.#e}getAudio(){return this.#i}setAudio(e){if(!(e instanceof HTMLAudioElement)&&"string"!=typeof e&&null!==e)throw new TypeError("sound must be an instance of HTMLAudioElement or null.");this.#i="string"!=typeof e?e:new Audio(e)}getBodyLimit(){return this.#n}setBodyLimit(e){if(!Number.isFinite(e)||e<0)throw new TypeError("bodyLimit must be a non-negative number.");this.#n=e}getDefaultAvatar(){return this.#o}setDefaultAvatar(e){if("string"!=typeof e&&null!==e)throw new TypeError("defaultIcon must be a string or null.");this.#o=e}getDefaultOnClick(){return this.#r}setDefaultOnClick(e){if("function"!=typeof e)throw new TypeError("defaultOnClick must be a function.");this.#r=e}};window.TinyNotifications=t.TinyNotifications})();
@@ -3459,34 +3459,105 @@ function areHtmlElsColliding(elem1, elem2) {
3459
3459
  }
3460
3460
 
3461
3461
  /**
3462
- * Reads and parses a JSON data using FileReader.
3463
- * Throws an error if the content is not valid JSON.
3464
- * @param {File} file
3465
- * @returns {Promise<any>}
3462
+ * Reads the contents of a file using the specified FileReader method.
3463
+ *
3464
+ * @param {File} file - The file to be read.
3465
+ * @param {'readAsArrayBuffer'|'readAsDataURL'|'readAsText'|'readAsBinaryString'} method -
3466
+ * The FileReader method to use for reading the file.
3467
+ * @returns {Promise<any>} - A promise that resolves with the file content, according to the chosen method.
3468
+ * @throws {Error} - If an unexpected error occurs while handling the result.
3469
+ * @throws {DOMException} - If the FileReader encounters an error while reading the file.
3466
3470
  */
3467
- function readJsonBlob(file) {
3471
+ function readFileBlob(file, method) {
3468
3472
  return new Promise((resolve, reject) => {
3469
3473
  const reader = new FileReader();
3470
-
3471
3474
  reader.onload = () => {
3472
3475
  try {
3473
- // @ts-ignore
3474
- const result = JSON.parse(reader.result);
3475
- resolve(result);
3476
+ resolve(reader.result);
3476
3477
  } catch (error) {
3477
- // @ts-ignore
3478
- reject(new Error(`Invalid JSON in file: ${file.name}\n${error.message}`));
3478
+ reject(error);
3479
3479
  }
3480
3480
  };
3481
-
3482
3481
  reader.onerror = () => {
3483
- reject(new Error(`Error reading file: ${file.name}`));
3482
+ reject(reader.error);
3484
3483
  };
3484
+ reader[method](file);
3485
+ });
3486
+ }
3485
3487
 
3486
- reader.readAsText(file);
3488
+ /**
3489
+ * Reads a file as a Base64 string using FileReader, and optionally formats it as a full data URL.
3490
+ *
3491
+ * Performs strict validation to ensure the result is a valid Base64 string or a proper data URL.
3492
+ *
3493
+ * @param {File} file - The file to be read.
3494
+ * @param {boolean|string} [isDataUrl=false] - If true, returns a full data URL; if false, returns only the Base64 string;
3495
+ * if a string is passed, it is used as the MIME type in the data URL.
3496
+ * @returns {Promise<string>} - A promise that resolves with the Base64 string or data URL.
3497
+ *
3498
+ * @throws {TypeError} - If the result is not a string or if `isDataUrl` is not a valid type.
3499
+ * @throws {Error} - If the result does not match the expected data URL format or Base64 structure.
3500
+ * @throws {DOMException} - If the FileReader fails to read the file.
3501
+ */
3502
+ function readBase64Blob(file, isDataUrl = false) {
3503
+ return new Promise((resolve, reject) => {
3504
+ if (typeof isDataUrl !== 'string' && typeof isDataUrl !== 'boolean')
3505
+ reject(new TypeError('The isDataUrl parameter must be a boolean or a string.'));
3506
+ readFileBlob(file, 'readAsDataURL')
3507
+ .then(
3508
+ /**
3509
+ * Ensure that the URL format is correct in the required pattern
3510
+ * @param {string} base64Data
3511
+ */ (base64Data) => {
3512
+ if (typeof base64Data !== 'string')
3513
+ throw new TypeError('Expected file content to be a string.');
3514
+
3515
+ const match = base64Data.match(/^data:(.+);base64,(.*)$/);
3516
+ if (!match || !match[2])
3517
+ throw new Error('Invalid data URL format or missing Base64 content.');
3518
+ const [, mimeType, base64] = match;
3519
+ if (!/^[\w/+]+=*$/.test(base64)) throw new Error('Base64 content is malformed.');
3520
+
3521
+ if (typeof isDataUrl === 'boolean') return resolve(isDataUrl ? base64Data : base64);
3522
+ if (!/^[\w-]+\/[\w.+-]+$/.test(isDataUrl))
3523
+ throw new Error(`Invalid MIME type string: ${isDataUrl}`);
3524
+
3525
+ return resolve(`data:${isDataUrl};base64,${base64}`);
3526
+ },
3527
+ )
3528
+ .catch(reject);
3487
3529
  });
3488
3530
  }
3489
3531
 
3532
+ /**
3533
+ * Reads a file and strictly validates its content as proper JSON using FileReader.
3534
+ *
3535
+ * Performs several checks to ensure the file contains valid, parsable JSON data.
3536
+ *
3537
+ * @param {File} file - The file to be read. It must contain valid JSON as plain text.
3538
+ * @returns {Promise<Record<string|number|symbol, any>|any[]>} - A promise that resolves with the parsed JSON object.
3539
+ *
3540
+ * @throws {SyntaxError} - If the file content is not valid JSON syntax.
3541
+ * @throws {TypeError} - If the result is not a string or does not represent a JSON value.
3542
+ * @throws {Error} - If the result is empty or structurally invalid as JSON.
3543
+ * @throws {DOMException} - If the FileReader fails to read the file.
3544
+ */
3545
+ function readJsonBlob(file) {
3546
+ return new Promise((resolve, reject) =>
3547
+ readFileBlob(file, 'readAsText')
3548
+ .then((data) => {
3549
+ if (typeof data !== 'string') throw new TypeError('Expected file content to be a string.');
3550
+ const trimmed = data.trim();
3551
+ if (trimmed.length === 0) throw new Error('File is empty or contains only whitespace.');
3552
+ const parsed = JSON.parse(trimmed);
3553
+ if (typeof parsed !== 'object' || parsed === null)
3554
+ throw new Error('Parsed content is not a valid JSON object or array.');
3555
+ resolve(parsed);
3556
+ })
3557
+ .catch(reject),
3558
+ );
3559
+ }
3560
+
3490
3561
  /**
3491
3562
  * Saves a JSON object as a downloadable file.
3492
3563
  * @param {string} filename
@@ -3589,7 +3660,8 @@ async function fetchJson(
3589
3660
 
3590
3661
  const data = await response.json();
3591
3662
 
3592
- if (!isJsonObject(data)) throw new Error('Received invalid data instead of valid JSON.');
3663
+ if (!Array.isArray(data) && !isJsonObject(data))
3664
+ throw new Error('Received invalid data instead of valid JSON.');
3593
3665
 
3594
3666
  return data;
3595
3667
  } catch (err) {
@@ -3691,19 +3763,34 @@ const getHtmlElPadding = (el) => {
3691
3763
 
3692
3764
  /**
3693
3765
  * Installs a script that toggles CSS classes on a given element
3694
- * based on the page's visibility or focus state.
3766
+ * based on the page's visibility or focus state, and optionally
3767
+ * triggers callbacks on visibility changes.
3695
3768
  *
3696
3769
  * @param {Object} [settings={}]
3697
3770
  * @param {HTMLElement} [settings.element=document.body] - The element to receive visibility classes.
3698
3771
  * @param {string} [settings.hiddenClass='windowHidden'] - CSS class applied when the page is hidden.
3699
3772
  * @param {string} [settings.visibleClass='windowVisible'] - CSS class applied when the page is visible.
3773
+ * @param {() => void} [settings.onVisible] - Callback called when page becomes visible.
3774
+ * @param {() => void} [settings.onHidden] - Callback called when page becomes hidden.
3700
3775
  * @returns {() => void} Function that removes all installed event listeners.
3776
+ * @throws {TypeError} If any provided setting is invalid.
3701
3777
  */
3702
3778
  function installWindowHiddenScript({
3703
3779
  element = document.body,
3704
3780
  hiddenClass = 'windowHidden',
3705
3781
  visibleClass = 'windowVisible',
3782
+ onVisible,
3783
+ onHidden,
3706
3784
  } = {}) {
3785
+ if (!(element instanceof HTMLElement))
3786
+ throw new TypeError(`"element" must be an instance of HTMLElement.`);
3787
+ if (typeof hiddenClass !== 'string') throw new TypeError(`"hiddenClass" must be a string.`);
3788
+ if (typeof visibleClass !== 'string') throw new TypeError(`"visibleClass" must be a string.`);
3789
+ if (onVisible !== undefined && typeof onVisible !== 'function')
3790
+ throw new TypeError(`"onVisible" must be a function if provided.`);
3791
+ if (onHidden !== undefined && typeof onHidden !== 'function')
3792
+ throw new TypeError(`"onHidden" must be a function if provided.`);
3793
+
3707
3794
  const removeClass = () => {
3708
3795
  element.classList.remove(hiddenClass);
3709
3796
  element.classList.remove(visibleClass);
@@ -3711,8 +3798,6 @@ function installWindowHiddenScript({
3711
3798
 
3712
3799
  /** @type {string|null} */
3713
3800
  let hiddenProp = null;
3714
- /** @type {(this: any, evt: Event) => void} */
3715
- let handler;
3716
3801
 
3717
3802
  const visibilityEvents = [
3718
3803
  'visibilitychange',
@@ -3730,7 +3815,8 @@ function installWindowHiddenScript({
3730
3815
  }
3731
3816
  }
3732
3817
 
3733
- handler = function (evt) {
3818
+ /** @type {(this: any, evt: Event) => void} */
3819
+ const handler = function (evt) {
3734
3820
  removeClass();
3735
3821
 
3736
3822
  const type = evt?.type;
@@ -3742,10 +3828,18 @@ function installWindowHiddenScript({
3742
3828
 
3743
3829
  if (visibleEvents.includes(type)) {
3744
3830
  element.classList.add(visibleClass);
3831
+ onVisible?.();
3745
3832
  } else if (hiddenEvents.includes(type)) {
3746
3833
  element.classList.add(hiddenClass);
3834
+ onHidden?.();
3747
3835
  } else {
3748
- element.classList.add(isHidden ? hiddenClass : visibleClass);
3836
+ if (isHidden) {
3837
+ element.classList.add(hiddenClass);
3838
+ onHidden?.();
3839
+ } else {
3840
+ element.classList.add(visibleClass);
3841
+ onVisible?.();
3842
+ }
3749
3843
  }
3750
3844
  };
3751
3845
 
@@ -3782,6 +3876,7 @@ function installWindowHiddenScript({
3782
3876
  };
3783
3877
  }
3784
3878
 
3879
+ // Trigger initial state
3785
3880
  // @ts-ignore
3786
3881
  const simulatedEvent = new Event(hiddenProp && document[hiddenProp] ? 'blur' : 'focus');
3787
3882
  handler(simulatedEvent);
@@ -5390,6 +5485,7 @@ class TinyDragger {
5390
5485
 
5391
5486
 
5392
5487
 
5488
+
5393
5489
 
5394
5490
 
5395
5491
  ;// ./src/v1/libs/TinyUploadClicker.mjs