tiny-essentials 1.10.2 → 1.11.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.
Files changed (56) hide show
  1. package/README.md +1 -1
  2. package/dist/{TinyEssentials.js → v1/TinyEssentials.js} +804 -0
  3. package/dist/v1/TinyEssentials.min.js +2 -0
  4. package/dist/v1/TinyNotifyCenter.js +461 -0
  5. package/dist/v1/TinyNotifyCenter.min.js +1 -0
  6. package/dist/v1/TinyToastNotify.js +417 -0
  7. package/dist/v1/TinyToastNotify.min.js +1 -0
  8. package/dist/v1/build/TinyNotifyCenter.cjs +7 -0
  9. package/dist/v1/build/TinyNotifyCenter.d.mts +3 -0
  10. package/dist/v1/build/TinyNotifyCenter.mjs +2 -0
  11. package/dist/v1/build/TinyToastNotify.cjs +7 -0
  12. package/dist/v1/build/TinyToastNotify.d.mts +3 -0
  13. package/dist/v1/build/TinyToastNotify.mjs +2 -0
  14. package/dist/v1/css/TinyNotify.css +350 -0
  15. package/dist/v1/css/TinyNotify.min.css +1 -0
  16. package/dist/v1/index.cjs +4 -0
  17. package/dist/v1/index.d.mts +3 -1
  18. package/dist/v1/index.mjs +3 -1
  19. package/dist/v1/libs/TinyNotifyCenter.cjs +422 -0
  20. package/dist/v1/libs/TinyNotifyCenter.d.mts +166 -0
  21. package/dist/v1/libs/TinyNotifyCenter.mjs +385 -0
  22. package/dist/v1/libs/TinyToastNotify.cjs +378 -0
  23. package/dist/v1/libs/TinyToastNotify.d.mts +157 -0
  24. package/dist/v1/libs/TinyToastNotify.mjs +332 -0
  25. package/docs/{README.md → v1/README.md} +2 -0
  26. package/docs/{basics → v1/basics}/text.md +2 -2
  27. package/docs/v1/libs/TinyNotifyCenter.md +291 -0
  28. package/docs/v1/libs/TinyToastNotify.md +290 -0
  29. package/package.json +4 -2
  30. package/dist/TinyEssentials.min.js +0 -2
  31. package/dist/v1/libs/TinyRateLimit.cjs +0 -196
  32. package/dist/v1/libs/TinyRateLimit.d.mts +0 -86
  33. package/dist/v1/libs/TinyRateLimit.mjs +0 -171
  34. /package/dist/{ColorSafeStringify.js → v1/ColorSafeStringify.js} +0 -0
  35. /package/dist/{ColorSafeStringify.min.js → v1/ColorSafeStringify.min.js} +0 -0
  36. /package/dist/{TinyBasicsEs.js → v1/TinyBasicsEs.js} +0 -0
  37. /package/dist/{TinyBasicsEs.min.js → v1/TinyBasicsEs.min.js} +0 -0
  38. /package/dist/{TinyBasicsEs.min.js.LICENSE.txt → v1/TinyBasicsEs.min.js.LICENSE.txt} +0 -0
  39. /package/dist/{TinyEssentials.min.js.LICENSE.txt → v1/TinyEssentials.min.js.LICENSE.txt} +0 -0
  40. /package/dist/{TinyLevelUp.js → v1/TinyLevelUp.js} +0 -0
  41. /package/dist/{TinyLevelUp.min.js → v1/TinyLevelUp.min.js} +0 -0
  42. /package/dist/{TinyPromiseQueue.js → v1/TinyPromiseQueue.js} +0 -0
  43. /package/dist/{TinyPromiseQueue.min.js → v1/TinyPromiseQueue.min.js} +0 -0
  44. /package/dist/{TinyRateLimiter.js → v1/TinyRateLimiter.js} +0 -0
  45. /package/dist/{TinyRateLimiter.min.js → v1/TinyRateLimiter.min.js} +0 -0
  46. /package/dist/{aiMarker.css → v1/css/aiMarker.css} +0 -0
  47. /package/dist/{aiMarker.min.css → v1/css/aiMarker.min.css} +0 -0
  48. /package/docs/{basics → v1/basics}/array.md +0 -0
  49. /package/docs/{basics → v1/basics}/asyncReplace.md +0 -0
  50. /package/docs/{basics → v1/basics}/clock.md +0 -0
  51. /package/docs/{basics → v1/basics}/objFilter.md +0 -0
  52. /package/docs/{basics → v1/basics}/simpleMath.md +0 -0
  53. /package/docs/{libs → v1/libs}/ColorSafeStringify.md +0 -0
  54. /package/docs/{libs → v1/libs}/TinyLevelUp.md +0 -0
  55. /package/docs/{libs → v1/libs}/TinyPromiseQueue.md +0 -0
  56. /package/docs/{libs → v1/libs}/TinyRateLimiter.md +0 -0
@@ -2144,8 +2144,10 @@ __webpack_require__.r(__webpack_exports__);
2144
2144
  __webpack_require__.d(__webpack_exports__, {
2145
2145
  ColorSafeStringify: () => (/* reexport */ libs_ColorSafeStringify),
2146
2146
  TinyLevelUp: () => (/* reexport */ userLevel),
2147
+ TinyNotifyCenter: () => (/* reexport */ libs_TinyNotifyCenter),
2147
2148
  TinyPromiseQueue: () => (/* reexport */ libs_TinyPromiseQueue),
2148
2149
  TinyRateLimiter: () => (/* reexport */ libs_TinyRateLimiter),
2150
+ TinyToastNotify: () => (/* reexport */ libs_TinyToastNotify),
2149
2151
  addAiMarkerShortcut: () => (/* reexport */ addAiMarkerShortcut),
2150
2152
  arraySortPositions: () => (/* reexport */ arraySortPositions),
2151
2153
  asyncReplace: () => (/* reexport */ asyncReplace),
@@ -4331,6 +4333,806 @@ class TinyRateLimiter {
4331
4333
 
4332
4334
  /* harmony default export */ const libs_TinyRateLimiter = (TinyRateLimiter);
4333
4335
 
4336
+ ;// ./src/v1/libs/TinyNotifyCenter.mjs
4337
+ /**
4338
+ * Represents a single notification entry.
4339
+ *
4340
+ * A notification can be provided as a simple string (treated as a plain message),
4341
+ * or as an object with additional data such as a title, an avatar image, and a click handler.
4342
+ *
4343
+ * @typedef {string | {
4344
+ * title?: string, // Optional title displayed above the message
4345
+ * message: string, // Required message content
4346
+ * avatar?: string, // Optional avatar image URL (displayed on the left)
4347
+ * onClick?: (e: MouseEvent) => void // Optional click handler for the entire notification
4348
+ * }} NotifyData
4349
+ */
4350
+
4351
+ /**
4352
+ * A notification center component for displaying interactive alerts in the UI.
4353
+ *
4354
+ * This class renders a notification overlay on the page and allows dynamically
4355
+ * adding, clearing, or interacting with notification items. Notifications can
4356
+ * contain plain text or HTML, and optionally support click events, titles, and avatars.
4357
+ *
4358
+ * Features:
4359
+ * - Dynamic rendering of notification UI with `insertTemplate()`
4360
+ * - Supports text and HTML content modes
4361
+ * - Optional avatars for each notification
4362
+ * - Callback support on notification click
4363
+ * - Per-notification close buttons
4364
+ * - Notification count badge
4365
+ *
4366
+ * @class
4367
+ */
4368
+ class TinyNotifyCenter {
4369
+ /**
4370
+ * Returns the full HTML structure for the notification system as a string.
4371
+ *
4372
+ * This includes:
4373
+ * - A hidden `.notify-overlay` containing the central notification panel (`#notifCenter`),
4374
+ * which has a header with a "Notifications" label, a "clear all" button, and a close button.
4375
+ * - A `.list` container for dynamically added notifications.
4376
+ * - A bell button (`.notify-bell`) to toggle the notification center, with an embedded badge.
4377
+ *
4378
+ * This template can be inserted into the DOM using `insertAdjacentHTML()` or parsed dynamically
4379
+ * into elements using JavaScript or jQuery, depending on the needs of the system.
4380
+ *
4381
+ * @returns {string} The complete HTML structure for the notification center.
4382
+ */
4383
+ static getTemplate() {
4384
+ return `
4385
+ <div class="notify-overlay hidden">
4386
+ <div class="notify-center" id="notifCenter">
4387
+ <div class="header">
4388
+ <div>Notifications</div>
4389
+ <div class="options">
4390
+ <button class="clear-all" type="button">
4391
+ <svg
4392
+ xmlns="http://www.w3.org/2000/svg"
4393
+ viewBox="0 0 24 24"
4394
+ width="24"
4395
+ height="24"
4396
+ fill="currentColor"
4397
+ >
4398
+ <path
4399
+ d="M21.6 2.4a1 1 0 0 0-1.4 0L13 9.6l-1.3-1.3a1 1 0 0 0-1.4 0L3 15.6a1 1 0 0 0 0 1.4l4 4a1 1 0 0 0 1.4 0l7.3-7.3a1 1 0 0 0 0-1.4l-1.3-1.3 7.2-7.2a1 1 0 0 0 0-1.4zM6 17l3.5-3.5 1.5 1.5L7.5 18.5 6 17z"
4400
+ />
4401
+ </svg>
4402
+ </button>
4403
+ <button class="close">×</button>
4404
+ </div>
4405
+ </div>
4406
+ <div class="list"></div>
4407
+ </div>
4408
+ </div>
4409
+
4410
+ <button class="notify-bell" aria-label="Open notifications">
4411
+ <svg
4412
+ xmlns="http://www.w3.org/2000/svg"
4413
+ width="20"
4414
+ height="20"
4415
+ fill="currentColor"
4416
+ viewBox="0 0 24 24"
4417
+ >
4418
+ <path
4419
+ d="M12 2C10.3 2 9 3.3 9 5v1.1C6.7 7.2 5 9.4 5 12v5l-1 1v1h16v-1l-1-1v-5c0-2.6-1.7-4.8-4-5.9V5c0-1.7-1.3-3-3-3zm0 20c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2z"
4420
+ />
4421
+ </svg>
4422
+ <span class="badge" id="notifBadge">0</span>
4423
+ </button>
4424
+ `;
4425
+ }
4426
+
4427
+ /**
4428
+ * Inserts the full notification center template into the document body.
4429
+ *
4430
+ * The structure is injected directly into the DOM using
4431
+ * `insertAdjacentHTML`.
4432
+ *
4433
+ * The `where` parameter allows control over where inside the `document.body`
4434
+ * the HTML is inserted:
4435
+ * - `'afterbegin'` (default): Inserts right after the opening <body> tag.
4436
+ * - `'beforeend'`: Inserts right before the closing </body> tag.
4437
+ * - Any valid position accepted by `insertAdjacentHTML`.
4438
+ *
4439
+ * @param {'beforebegin'|'afterbegin'|'beforeend'|'afterend'} [where='afterbegin']
4440
+ * The position relative to `document.body` where the HTML should be inserted.
4441
+ */
4442
+ static insertTemplate(where = 'afterbegin') {
4443
+ document.body.insertAdjacentHTML(where, TinyNotifyCenter.getTemplate());
4444
+ }
4445
+
4446
+ /** @type {HTMLElement} */
4447
+ #center;
4448
+ /** @type {HTMLElement} */
4449
+ #list;
4450
+ /** @type {HTMLElement} */
4451
+ #badge;
4452
+ /** @type {HTMLElement} */
4453
+ #button;
4454
+ /** @type {HTMLElement} */
4455
+ #overlay;
4456
+
4457
+ #count = 0;
4458
+ #maxCount = 99;
4459
+ #removeDelay = 300;
4460
+ #markAllAsReadOnClose = false;
4461
+ #modes = new WeakMap();
4462
+
4463
+ /** @param {HTMLElement|ChildNode} item */
4464
+ #removeItem(item) {
4465
+ this.#modes.delete(item);
4466
+ if (item instanceof HTMLElement) {
4467
+ item.classList.add('removing');
4468
+
4469
+ setTimeout(() => {
4470
+ this.markAsRead(item);
4471
+ item.remove();
4472
+ }, this.#removeDelay);
4473
+ } else throw new Error('Invalid HTMLElement to clear.');
4474
+ }
4475
+
4476
+ /**
4477
+ * Update notify count and badge.
4478
+ * @param {number} value
4479
+ */
4480
+ #updateCount(value) {
4481
+ this.#count = Math.max(0, value);
4482
+ this.#badge.setAttribute('data-value', String(this.#count));
4483
+ this.#badge.textContent =
4484
+ this.#count > this.#maxCount ? `${this.#maxCount}+` : String(this.#count);
4485
+ }
4486
+
4487
+ /**
4488
+ * Options for configuring the NotificationCenter instance.
4489
+ *
4490
+ * Allows manual specification of the main elements used by the notification center.
4491
+ * If not provided, default elements will be selected from the DOM automatically.
4492
+ *
4493
+ * @param {Object} options - Configuration object.
4494
+ * @param {HTMLElement} [options.center=document.getElementById('notifCenter')] - The container element that holds the list of notifications.
4495
+ * @param {HTMLElement} [options.badge=document.getElementById('notifBadge')] - The badge element used to display the current notification count.
4496
+ * @param {HTMLElement} [options.button=document.querySelector('.notify-bell')] - The button element that toggles the notification center.
4497
+ * @param {HTMLElement} [options.overlay=document.querySelector('.notify-overlay')] - The overlay element that covers the screen when the center is visible.
4498
+ */
4499
+ constructor(options = {}) {
4500
+ const {
4501
+ center = document.getElementById('notifCenter'),
4502
+ badge = document.getElementById('notifBadge'),
4503
+ button = document.querySelector('.notify-bell'),
4504
+ overlay = document.querySelector('.notify-overlay'),
4505
+ } = options;
4506
+
4507
+ // Element existence and type validation
4508
+ if (!(center instanceof HTMLElement))
4509
+ throw new Error(`NotificationCenter: "center" must be an HTMLElement. Got: ${center}`);
4510
+ if (!(overlay instanceof HTMLElement))
4511
+ throw new Error(`NotificationCenter: "overlay" must be an HTMLElement. Got: ${overlay}`);
4512
+ if (!(badge instanceof HTMLElement))
4513
+ throw new Error(`NotificationCenter: "badge" must be an HTMLElement. Got: ${badge}`);
4514
+ if (!(button instanceof HTMLElement))
4515
+ throw new Error(`NotificationCenter: "button" must be an HTMLElement. Got: ${button}`);
4516
+
4517
+ const clearAllBtn = center?.querySelector('.clear-all');
4518
+ const list = center?.querySelector('.list') ?? null;
4519
+ if (!(list instanceof HTMLElement))
4520
+ throw new Error(
4521
+ `NotificationCenter: ".list" inside center must be an HTMLElement. Got: ${list}`,
4522
+ );
4523
+
4524
+ this.#center = center;
4525
+ this.#list = list;
4526
+ this.#badge = badge;
4527
+ this.#button = button;
4528
+ this.#overlay = overlay;
4529
+
4530
+ this.#button.addEventListener('click', () => this.toggle());
4531
+ this.#center.querySelector('.close')?.addEventListener('click', () => this.close());
4532
+ if (clearAllBtn) clearAllBtn.addEventListener('click', () => this.clear());
4533
+ this.#overlay.addEventListener('click', (e) => {
4534
+ if (e.target === this.#overlay) this.close();
4535
+ });
4536
+ }
4537
+
4538
+ /**
4539
+ * Enable or disable automatic mark-as-read on close.
4540
+ * @param {boolean} value
4541
+ */
4542
+ setMarkAllAsReadOnClose(value) {
4543
+ if (typeof value !== 'boolean')
4544
+ throw new TypeError(`Expected boolean for markAllAsReadOnClose, got ${typeof value}`);
4545
+ this.#markAllAsReadOnClose = value;
4546
+ }
4547
+
4548
+ /**
4549
+ * Define how long the remove animation takes (in ms).
4550
+ * @param {number} ms
4551
+ */
4552
+ setRemoveDelay(ms) {
4553
+ if (typeof ms !== 'number') throw new Error(`NotificationCenter: "ms" must be an number.`);
4554
+ this.#removeDelay = ms;
4555
+ }
4556
+
4557
+ /**
4558
+ * Get rendering mode ('text' or 'html') by index.
4559
+ * @param {number} index
4560
+ * @returns {'text' | 'html' | null}
4561
+ */
4562
+ getItemMode(index) {
4563
+ const item = this.getItem(index);
4564
+ return item ? this.#modes.get(item) : null;
4565
+ }
4566
+
4567
+ /**
4568
+ * Get a notify element by index.
4569
+ * @param {number} index
4570
+ * @returns {HTMLElement}
4571
+ */
4572
+ getItem(index) {
4573
+ const element = this.#list.children.item(index);
4574
+ if (!(element instanceof HTMLElement))
4575
+ throw new Error(`NotificationCenter: "item" must be an HTMLElement. Got: ${element}`);
4576
+ return element;
4577
+ }
4578
+
4579
+ /**
4580
+ * Check if a notify exists at the given index.
4581
+ * @param {number} index
4582
+ * @returns {boolean}
4583
+ */
4584
+ hasItem(index) {
4585
+ return index >= 0 && index < this.#list.children.length;
4586
+ }
4587
+
4588
+ /**
4589
+ * Mark a notification index as read.
4590
+ * @param {number|HTMLElement} index
4591
+ */
4592
+ markAsRead(index) {
4593
+ const item = index instanceof HTMLElement ? index : this.getItem(index);
4594
+ if (item.classList.contains('unread')) {
4595
+ item.classList.remove('unread');
4596
+ this.#updateCount(this.#count - 1);
4597
+ }
4598
+ }
4599
+
4600
+ /**
4601
+ * Add a new notify to the center.
4602
+ *
4603
+ * @param {NotifyData} message - Notification content or a full object with title, avatar, and callback.
4604
+ * @param {'text'|'html'} [mode='text'] - How to treat the message content.
4605
+ */
4606
+ add(message, mode = 'text') {
4607
+ const item = document.createElement('div');
4608
+ item.className = 'item unread';
4609
+
4610
+ let titleText = null;
4611
+ let messageText = null;
4612
+ let avatarUrl = null;
4613
+ let onClick = null;
4614
+
4615
+ if (typeof message === 'object' && message !== null) {
4616
+ titleText = message.title;
4617
+ messageText = message.message;
4618
+ avatarUrl = message.avatar;
4619
+ onClick = message.onClick;
4620
+ } else {
4621
+ messageText = message;
4622
+ }
4623
+
4624
+ // Optional avatar
4625
+ if (avatarUrl) {
4626
+ const avatarElem = document.createElement('div');
4627
+ avatarElem.className = 'avatar';
4628
+ avatarElem.style.backgroundImage = `url("${avatarUrl}")`;
4629
+ item.appendChild(avatarElem);
4630
+ }
4631
+
4632
+ // Content wrapper
4633
+ const contentWrapper = document.createElement('div');
4634
+ contentWrapper.className = 'content';
4635
+
4636
+ // Optional title
4637
+ if (titleText) {
4638
+ const titleElem = document.createElement('div');
4639
+ titleElem.className = 'title';
4640
+ titleElem.textContent = titleText;
4641
+ contentWrapper.appendChild(titleElem);
4642
+ }
4643
+
4644
+ // Message
4645
+ const messageElem = document.createElement('div');
4646
+ messageElem.className = 'message';
4647
+
4648
+ if (mode === 'html') {
4649
+ messageElem.innerHTML = messageText;
4650
+ } else {
4651
+ messageElem.textContent = messageText;
4652
+ }
4653
+
4654
+ contentWrapper.appendChild(messageElem);
4655
+
4656
+ // Action by clicking (if provided)
4657
+ if (typeof onClick === 'function') {
4658
+ item.classList.add('clickable');
4659
+ item.addEventListener('click', (e) => {
4660
+ // Prevents the close button from clicking
4661
+ if (e.target instanceof HTMLElement && !e.target.closest('.notify-close')) {
4662
+ onClick(e);
4663
+ }
4664
+ });
4665
+ }
4666
+
4667
+ // Close button
4668
+ const closeBtn = document.createElement('button');
4669
+ closeBtn.className = 'notify-close';
4670
+ closeBtn.setAttribute('type', 'button');
4671
+ closeBtn.innerHTML = '&times;';
4672
+ closeBtn.addEventListener('click', (e) => {
4673
+ e.stopPropagation(); // prevents propagation for the main onClick
4674
+ this.#removeItem(item);
4675
+ });
4676
+
4677
+ item.append(contentWrapper, closeBtn);
4678
+ this.#list.prepend(item);
4679
+ this.#modes.set(item, mode);
4680
+ this.#updateCount(this.#count + 1);
4681
+ }
4682
+
4683
+ /**
4684
+ * Remove a notify by index.
4685
+ * @param {number} index
4686
+ */
4687
+ remove(index) {
4688
+ const item = this.getItem(index);
4689
+ this.#removeItem(item);
4690
+ }
4691
+
4692
+ /**
4693
+ * Clear all notifications safely.
4694
+ */
4695
+ clear() {
4696
+ let needAgain = true;
4697
+ while (needAgain) {
4698
+ needAgain = false;
4699
+ const items = Array.from(this.#list.children);
4700
+ for (const item of items) {
4701
+ if (item instanceof HTMLElement && !item.classList.contains('removing')) {
4702
+ this.#removeItem(item);
4703
+ needAgain = true;
4704
+ }
4705
+ }
4706
+ }
4707
+ }
4708
+
4709
+ /**
4710
+ * Open the notify center.
4711
+ */
4712
+ open() {
4713
+ this.#overlay.classList.remove('hidden');
4714
+ this.#center.classList.add('open');
4715
+ }
4716
+
4717
+ /**
4718
+ * Close the notify center.
4719
+ */
4720
+ close() {
4721
+ this.#overlay.classList.add('hidden');
4722
+ this.#center.classList.remove('open');
4723
+ if (this.#markAllAsReadOnClose) {
4724
+ const items = this.#list.querySelectorAll('.item.unread');
4725
+ for (const item of items) {
4726
+ if (item instanceof HTMLElement) this.markAsRead(item);
4727
+ }
4728
+ }
4729
+ }
4730
+
4731
+ /**
4732
+ * Toggle open/close state.
4733
+ */
4734
+ toggle() {
4735
+ if (this.#center.classList.contains('open')) this.close();
4736
+ else this.open();
4737
+ }
4738
+
4739
+ /**
4740
+ * Recalculate the number of notifications based on the actual DOM list.
4741
+ */
4742
+ recount() {
4743
+ const count = this.#list.querySelectorAll('.item.unread').length;
4744
+ this.#updateCount(count);
4745
+ }
4746
+
4747
+ /**
4748
+ * Get current count.
4749
+ * @returns {number}
4750
+ */
4751
+ get count() {
4752
+ return this.#count;
4753
+ }
4754
+ }
4755
+
4756
+ /* harmony default export */ const libs_TinyNotifyCenter = (TinyNotifyCenter);
4757
+
4758
+ ;// ./src/v1/libs/TinyToastNotify.mjs
4759
+ /**
4760
+ * A callback function used to manually close a notification.
4761
+ * Passed as a second argument to `onClick` handlers, allowing programmatic dismissal of the toast.
4762
+ *
4763
+ * @typedef {() => void} CloseToastFunc
4764
+ */
4765
+
4766
+ /**
4767
+ * Represents the data used to display a notification.
4768
+ * Can be a plain string (used as the message), or an object with more customization options.
4769
+ *
4770
+ * @typedef {string | {
4771
+ * message: string, // The main message to display
4772
+ * title?: string, // Optional title to appear above the message
4773
+ * onClick?: function(MouseEvent, CloseToastFunc): void, // Optional click handler for the notification
4774
+ * html?: boolean, // Whether the message should be interpreted as raw HTML
4775
+ * avatar?: string // Optional URL to an avatar image shown on the left
4776
+ * }} NotifyData
4777
+ */
4778
+
4779
+ /**
4780
+ * A lightweight notification system designed to display timed messages inside a container.
4781
+ * Supports positioning, timing customization, click actions, HTML content, and optional avatars.
4782
+ *
4783
+ * ## Features:
4784
+ * - Positioning via `x` (`left`, `center`, `right`) and `y` (`top`, `bottom`).
4785
+ * - Dynamic display time based on message length.
4786
+ * - Optional `title`, `avatar`, `onClick`, and `html` message rendering.
4787
+ * - Fade-out animation with customizable duration.
4788
+ * - Rigid validation of inputs and internal state.
4789
+ *
4790
+ * ## Customization via setters:
4791
+ * - `setX(position)` — horizontal alignment.
4792
+ * - `setY(position)` — vertical alignment.
4793
+ * - `setBaseDuration(ms)` — base visible time in milliseconds.
4794
+ * - `setExtraPerChar(ms)` — extra time added per character.
4795
+ * - `setFadeOutDuration(ms)` — fade-out animation duration in milliseconds.
4796
+ *
4797
+ * @class
4798
+ */
4799
+ class TinyToastNotify {
4800
+ #y;
4801
+ #x;
4802
+ #baseDuration;
4803
+ #extraPerChar;
4804
+ #fadeOutDuration;
4805
+ #container;
4806
+
4807
+ /**
4808
+ * @param {'top'|'bottom'} y - 'top' or 'bottom'
4809
+ * @param {'right'|'left'|'center'} x - 'right', 'left', or 'center'
4810
+ * @param {number} baseDuration - Base display time in ms
4811
+ * @param {number} extraPerChar - Extra ms per character
4812
+ * @param {number} fadeOutDuration - Time in ms for fade-out effect
4813
+ * @param {string} [selector='.notify-container'] - Base selector for container
4814
+ */
4815
+ constructor(
4816
+ y = 'top',
4817
+ x = 'right',
4818
+ baseDuration = 3000,
4819
+ extraPerChar = 50,
4820
+ fadeOutDuration = 300,
4821
+ selector = '.notify-container',
4822
+ ) {
4823
+ this.#validateY(y);
4824
+ this.#validateX(x);
4825
+ this.#validateTiming(baseDuration, 'baseDuration');
4826
+ this.#validateTiming(extraPerChar, 'extraPerChar');
4827
+ this.#validateTiming(fadeOutDuration, 'fadeOutDuration');
4828
+
4829
+ this.#y = y;
4830
+ this.#x = x;
4831
+ this.#baseDuration = baseDuration;
4832
+ this.#extraPerChar = extraPerChar;
4833
+ this.#fadeOutDuration = fadeOutDuration;
4834
+ const container = document.querySelector(`${selector}.${y}.${x}`);
4835
+
4836
+ if (!(container instanceof HTMLElement)) {
4837
+ this.#container = document.createElement('div');
4838
+ this.#container.className = `notify-container ${y} ${x}`;
4839
+ document.body.appendChild(this.#container);
4840
+ } else this.#container = container;
4841
+ }
4842
+
4843
+ /**
4844
+ * Returns the notification container element.
4845
+ * Ensures that the container is a valid HTMLElement.
4846
+ *
4847
+ * @returns {HTMLElement} The notification container.
4848
+ * @throws {Error} If the container is not a valid HTMLElement.
4849
+ */
4850
+ getContainer() {
4851
+ if (!(this.#container instanceof HTMLElement))
4852
+ throw new Error('Container is not a valid HTMLElement.');
4853
+ return this.#container;
4854
+ }
4855
+
4856
+ /**
4857
+ * Validates the vertical position value.
4858
+ * Must be either 'top' or 'bottom'.
4859
+ *
4860
+ * @param {string} value - The vertical position to validate.
4861
+ * @throws {Error} If the value is not 'top' or 'bottom'.
4862
+ */
4863
+ #validateY(value) {
4864
+ if (!['top', 'bottom'].includes(value)) {
4865
+ throw new Error(`Invalid vertical direction "${value}". Expected "top" or "bottom".`);
4866
+ }
4867
+ }
4868
+
4869
+ /**
4870
+ * Validates the horizontal position value.
4871
+ * Must be 'left', 'right', or 'center'.
4872
+ *
4873
+ * @param {string} value - The horizontal position to validate.
4874
+ * @throws {Error} If the value is not one of the accepted directions.
4875
+ */
4876
+ #validateX(value) {
4877
+ if (!['left', 'right', 'center'].includes(value)) {
4878
+ throw new Error(
4879
+ `Invalid horizontal position "${value}". Expected "left", "right" or "center".`,
4880
+ );
4881
+ }
4882
+ }
4883
+
4884
+ /**
4885
+ * Validates a numeric timing value.
4886
+ * Must be a non-negative finite number.
4887
+ *
4888
+ * @param {number} value - The number to validate.
4889
+ * @param {string} name - The name of the parameter (used for error messaging).
4890
+ * @throws {Error} If the number is invalid.
4891
+ */
4892
+ #validateTiming(value, name) {
4893
+ if (typeof value !== 'number' || value < 0 || !Number.isFinite(value)) {
4894
+ throw new Error(
4895
+ `Invalid value for "${name}": ${value}. Must be a non-negative finite number.`,
4896
+ );
4897
+ }
4898
+ }
4899
+
4900
+ /**
4901
+ * Returns the current vertical position.
4902
+ *
4903
+ * @returns {'top'|'bottom'} The vertical direction of the notification container.
4904
+ */
4905
+ getY() {
4906
+ return this.#y;
4907
+ }
4908
+
4909
+ /**
4910
+ * Sets the vertical position of the notification container.
4911
+ * Updates the container's class to reflect the new position.
4912
+ *
4913
+ * @param {'top'|'bottom'} value - The vertical direction to set.
4914
+ * @throws {Error} If the value is invalid.
4915
+ */
4916
+ setY(value) {
4917
+ this.#validateY(value);
4918
+ const container = this.getContainer();
4919
+ container.classList.remove(this.#y);
4920
+ container.classList.add(value);
4921
+
4922
+ this.#y = value;
4923
+ }
4924
+
4925
+ /**
4926
+ * Returns the current horizontal position.
4927
+ *
4928
+ * @returns {'left'|'right'|'center'} The horizontal direction of the notification container.
4929
+ */
4930
+ getX() {
4931
+ return this.#x;
4932
+ }
4933
+
4934
+ /**
4935
+ * Sets the horizontal position of the notification container.
4936
+ * Updates the container's class to reflect the new position.
4937
+ *
4938
+ * @param {'left'|'right'|'center'} value - The horizontal direction to set.
4939
+ * @throws {Error} If the value is invalid.
4940
+ */
4941
+ setX(value) {
4942
+ this.#validateX(value);
4943
+ const container = this.getContainer();
4944
+ container.classList.remove(this.#x);
4945
+ container.classList.add(value);
4946
+
4947
+ this.#x = value;
4948
+ }
4949
+
4950
+ /**
4951
+ * Returns the base duration for displaying the notification.
4952
+ *
4953
+ * @returns {number} Base time (in milliseconds) that a notification stays on screen.
4954
+ */
4955
+ getBaseDuration() {
4956
+ return this.#baseDuration;
4957
+ }
4958
+
4959
+ /**
4960
+ * Sets the base duration for the notification display time.
4961
+ *
4962
+ * @param {number} value - Base display time in milliseconds.
4963
+ * @throws {Error} If the value is not a valid non-negative finite number.
4964
+ */
4965
+ setBaseDuration(value) {
4966
+ this.#validateTiming(value, 'baseDuration');
4967
+ this.#baseDuration = value;
4968
+ }
4969
+
4970
+ /**
4971
+ * Returns the extra display time added per character.
4972
+ *
4973
+ * @returns {number} Extra time (in milliseconds) per character in the notification.
4974
+ */
4975
+ getExtraPerChar() {
4976
+ return this.#extraPerChar;
4977
+ }
4978
+
4979
+ /**
4980
+ * Sets the additional display time per character.
4981
+ *
4982
+ * @param {number} value - Extra time in milliseconds per character.
4983
+ * @throws {Error} If the value is not a valid non-negative finite number.
4984
+ */
4985
+ setExtraPerChar(value) {
4986
+ this.#validateTiming(value, 'extraPerChar');
4987
+ this.#extraPerChar = value;
4988
+ }
4989
+
4990
+ /**
4991
+ * Returns the fade-out duration.
4992
+ *
4993
+ * @returns {number} Time (in milliseconds) used for fade-out transition.
4994
+ */
4995
+ getFadeOutDuration() {
4996
+ return this.#fadeOutDuration;
4997
+ }
4998
+
4999
+ /**
5000
+ * Sets the fade-out transition time for notifications.
5001
+ *
5002
+ * @param {number} value - Fade-out duration in milliseconds.
5003
+ * @throws {Error} If the value is not a valid non-negative finite number.
5004
+ */
5005
+ setFadeOutDuration(value) {
5006
+ this.#validateTiming(value, 'fadeOutDuration');
5007
+ this.#fadeOutDuration = value;
5008
+ }
5009
+
5010
+ /**
5011
+ * Displays a notification for a time based on message length.
5012
+ * Accepts a string or an object with:
5013
+ * {
5014
+ * message: string,
5015
+ * title?: string,
5016
+ * onClick?: function(MouseEvent, CloseToastFunc): void,
5017
+ * html?: boolean,
5018
+ * avatar?: string // Optional avatar image URL
5019
+ * }
5020
+ *
5021
+ * @param {NotifyData} data
5022
+ */
5023
+ show(data) {
5024
+ let message = '';
5025
+ let title = '';
5026
+ let onClick = null;
5027
+ let useHTML = false;
5028
+ let avatarUrl = null;
5029
+
5030
+ const notify = document.createElement('div');
5031
+ notify.className = 'notify enter';
5032
+
5033
+ if (typeof data === 'string') {
5034
+ message = data;
5035
+ } else if (typeof data === 'object' && data !== null && typeof data.message === 'string') {
5036
+ message = data.message;
5037
+ title = typeof data.title === 'string' ? data.title : '';
5038
+ useHTML = data.html === true;
5039
+ avatarUrl = typeof data.avatar === 'string' ? data.avatar : null;
5040
+
5041
+ if (data.onClick !== undefined) {
5042
+ if (typeof data.onClick !== 'function') {
5043
+ throw new Error('onClick must be a function if defined');
5044
+ }
5045
+ onClick = data.onClick;
5046
+ notify.classList.add('clickable');
5047
+ }
5048
+ } else {
5049
+ throw new Error(
5050
+ `Invalid argument for show(): expected string or { message: string, title?: string, onClick?: function, html?: boolean, avatar?: string }`,
5051
+ );
5052
+ }
5053
+
5054
+ // Add close button
5055
+ const closeBtn = document.createElement('button');
5056
+ closeBtn.innerHTML = '&times;';
5057
+ closeBtn.className = 'close';
5058
+ closeBtn.setAttribute('aria-label', 'Close');
5059
+
5060
+ // Optional hover effect
5061
+ closeBtn.addEventListener('mouseenter', () => {
5062
+ closeBtn.style.color = 'var(--notif-close-color-hover)';
5063
+ });
5064
+ closeBtn.addEventListener('mouseleave', () => {
5065
+ closeBtn.style.color = 'var(--notif-close-color)';
5066
+ });
5067
+
5068
+ // Avatar
5069
+ if (avatarUrl) {
5070
+ const avatar = document.createElement('img');
5071
+ avatar.src = avatarUrl;
5072
+ avatar.alt = 'avatar';
5073
+ avatar.className = 'avatar';
5074
+ notify.appendChild(avatar);
5075
+ }
5076
+
5077
+ // Title
5078
+ if (title) {
5079
+ const titleElem = document.createElement('strong');
5080
+ titleElem.textContent = title;
5081
+ titleElem.style.display = 'block';
5082
+ notify.appendChild(titleElem);
5083
+ }
5084
+
5085
+ // Message
5086
+ if (useHTML) {
5087
+ const msgWrapper = document.createElement('div');
5088
+ msgWrapper.innerHTML = message;
5089
+ notify.appendChild(msgWrapper);
5090
+ } else {
5091
+ notify.appendChild(document.createTextNode(message));
5092
+ }
5093
+
5094
+ notify.appendChild(closeBtn);
5095
+ this.getContainer().appendChild(notify);
5096
+
5097
+ const visibleTime = this.#baseDuration + message.length * this.#extraPerChar;
5098
+ const totalTime = visibleTime + this.#fadeOutDuration;
5099
+
5100
+ // Close logic
5101
+ let removed = false;
5102
+ const close = () => {
5103
+ if (removed) return;
5104
+ removed = true;
5105
+ notify.classList.remove('enter', 'show');
5106
+ notify.classList.add('exit');
5107
+ setTimeout(() => notify.remove(), this.#fadeOutDuration);
5108
+ };
5109
+
5110
+ // Click handler
5111
+ if (typeof onClick === 'function') {
5112
+ notify.addEventListener('click', (e) => {
5113
+ if (e.target === closeBtn) return;
5114
+ onClick(e, close);
5115
+ });
5116
+ }
5117
+
5118
+ // Close button click
5119
+ closeBtn.addEventListener('click', (e) => {
5120
+ e.stopPropagation();
5121
+ close();
5122
+ });
5123
+
5124
+ // Transition activation force soon after the element is added
5125
+ setTimeout(() => {
5126
+ notify.classList.remove('enter');
5127
+ notify.classList.add('show');
5128
+ }, 1);
5129
+
5130
+ setTimeout(() => close(), totalTime);
5131
+ }
5132
+ }
5133
+
5134
+ /* harmony default export */ const libs_TinyToastNotify = (TinyToastNotify);
5135
+
4334
5136
  ;// ./src/v1/index.mjs
4335
5137
 
4336
5138
 
@@ -4346,6 +5148,8 @@ class TinyRateLimiter {
4346
5148
 
4347
5149
 
4348
5150
 
5151
+
5152
+
4349
5153
  })();
4350
5154
 
4351
5155
  window.TinyEssentials = __webpack_exports__;