@incursa/ui-kit 0.3.7 → 0.4.1

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.
@@ -6,6 +6,8 @@
6
6
  menu: ".inc-dropdown__menu",
7
7
  collapseToggle: '[data-inc-toggle="collapse"]',
8
8
  tabToggle: '[data-inc-toggle="tab"]',
9
+ autoRefresh: "[data-inc-auto-refresh]",
10
+ autoRefreshToggle: '[data-inc-action="auto-refresh-toggle"]',
9
11
  modalToggle: '[data-inc-toggle="modal"]',
10
12
  modalDismiss: '[data-inc-dismiss="modal"]',
11
13
  offcanvasToggle: '[data-inc-toggle="offcanvas"]',
@@ -25,6 +27,9 @@
25
27
  '[tabindex]:not([tabindex="-1"])',
26
28
  ].join(", ");
27
29
 
30
+ const autoRefreshControllers = [];
31
+ let autoRefreshReloadScheduled = false;
32
+
28
33
  function getTarget(trigger) {
29
34
  const rawTarget = trigger.getAttribute("data-inc-target")
30
35
  || trigger.getAttribute("href")
@@ -380,6 +385,278 @@
380
385
  return overlays[overlays.length - 1] || null;
381
386
  }
382
387
 
388
+ function parsePositiveInteger(value) {
389
+ const parsed = Number.parseInt(value || "", 10);
390
+
391
+ if (!Number.isFinite(parsed) || parsed < 1) {
392
+ return null;
393
+ }
394
+
395
+ return parsed;
396
+ }
397
+
398
+ function formatAutoRefreshRemaining(totalSeconds) {
399
+ if (totalSeconds < 60) {
400
+ return `${totalSeconds}s`;
401
+ }
402
+
403
+ const minutes = Math.floor(totalSeconds / 60);
404
+ const seconds = totalSeconds % 60;
405
+ return `${minutes}m ${seconds}s`;
406
+ }
407
+
408
+ function getAutoRefreshParts(root) {
409
+ return {
410
+ countdown: root.querySelector(".inc-auto-refresh__countdown"),
411
+ label: root.querySelector(".inc-auto-refresh__label"),
412
+ value: root.querySelector(".inc-auto-refresh__value"),
413
+ status: root.querySelector(".inc-auto-refresh__status"),
414
+ statusText: root.querySelector(".inc-auto-refresh__status-text"),
415
+ toggle: root.querySelector(".inc-auto-refresh__toggle"),
416
+ toggleText: root.querySelector(".inc-auto-refresh__toggle-text"),
417
+ };
418
+ }
419
+
420
+ function updateAutoRefreshToggle(controller) {
421
+ const { parts, isPaused, isLoading, pauseActionLabel, resumeActionLabel } = controller;
422
+
423
+ if (!(parts.toggle instanceof HTMLElement)) {
424
+ return;
425
+ }
426
+
427
+ const actionLabel = isPaused ? resumeActionLabel : pauseActionLabel;
428
+ parts.toggle.disabled = Boolean(isLoading);
429
+ parts.toggle.setAttribute("aria-pressed", isPaused ? "true" : "false");
430
+ parts.toggle.setAttribute("aria-label", actionLabel);
431
+
432
+ if (parts.toggleText) {
433
+ parts.toggleText.textContent = actionLabel;
434
+ }
435
+ }
436
+
437
+ function renderAutoRefreshCountdown(controller, remainingSeconds) {
438
+ const { root, parts, refreshLabel } = controller;
439
+
440
+ if (parts.label) {
441
+ parts.label.textContent = refreshLabel;
442
+ }
443
+
444
+ if (parts.value) {
445
+ parts.value.textContent = formatAutoRefreshRemaining(remainingSeconds);
446
+ }
447
+
448
+ root.classList.remove("is-paused");
449
+ root.classList.remove("is-loading");
450
+ root.setAttribute("aria-busy", "false");
451
+
452
+ if (parts.countdown) {
453
+ parts.countdown.hidden = false;
454
+ }
455
+
456
+ if (parts.status) {
457
+ parts.status.hidden = true;
458
+ }
459
+
460
+ updateAutoRefreshToggle(controller);
461
+ }
462
+
463
+ function renderAutoRefreshPaused(controller, remainingSeconds) {
464
+ const { root, parts, pausedLabel } = controller;
465
+
466
+ if (parts.label) {
467
+ parts.label.textContent = pausedLabel;
468
+ }
469
+
470
+ if (parts.value) {
471
+ parts.value.textContent = formatAutoRefreshRemaining(remainingSeconds);
472
+ }
473
+
474
+ root.classList.add("is-paused");
475
+ root.classList.remove("is-loading");
476
+ root.setAttribute("aria-busy", "false");
477
+
478
+ if (parts.countdown) {
479
+ parts.countdown.hidden = false;
480
+ }
481
+
482
+ if (parts.status) {
483
+ parts.status.hidden = true;
484
+ }
485
+
486
+ updateAutoRefreshToggle(controller);
487
+ }
488
+
489
+ function setAutoRefreshLoadingState(controller) {
490
+ const { root, parts, loadingLabel } = controller;
491
+
492
+ root.classList.remove("is-paused");
493
+ root.classList.add("is-loading");
494
+ root.setAttribute("aria-busy", "true");
495
+
496
+ if (parts.countdown) {
497
+ parts.countdown.hidden = true;
498
+ }
499
+
500
+ if (parts.statusText) {
501
+ parts.statusText.textContent = loadingLabel;
502
+ }
503
+
504
+ if (parts.status) {
505
+ parts.status.hidden = false;
506
+ }
507
+
508
+ updateAutoRefreshToggle(controller);
509
+ }
510
+
511
+ function stopAutoRefreshController(controller) {
512
+ if (controller.timeoutId) {
513
+ window.clearTimeout(controller.timeoutId);
514
+ controller.timeoutId = 0;
515
+ }
516
+ }
517
+
518
+ function pauseAutoRefresh(controller) {
519
+ if (autoRefreshReloadScheduled || controller.isLoading || controller.isPaused) {
520
+ return;
521
+ }
522
+
523
+ controller.isPaused = true;
524
+ controller.remainingMs = Math.max(controller.deadline - Date.now(), 0);
525
+ stopAutoRefreshController(controller);
526
+ renderAutoRefreshPaused(controller, Math.max(1, Math.ceil(controller.remainingMs / 1000)));
527
+ }
528
+
529
+ function resumeAutoRefresh(controller) {
530
+ if (autoRefreshReloadScheduled || controller.isLoading || !controller.isPaused) {
531
+ return;
532
+ }
533
+
534
+ controller.isPaused = false;
535
+ controller.deadline = Date.now() + controller.remainingMs;
536
+ controller.remainingMs = 0;
537
+ scheduleAutoRefreshTick(controller);
538
+ }
539
+
540
+ function toggleAutoRefresh(controller) {
541
+ if (controller.isPaused) {
542
+ resumeAutoRefresh(controller);
543
+ return;
544
+ }
545
+
546
+ pauseAutoRefresh(controller);
547
+ }
548
+
549
+ function scheduleWindowReload() {
550
+ if (autoRefreshReloadScheduled) {
551
+ return;
552
+ }
553
+
554
+ autoRefreshReloadScheduled = true;
555
+ autoRefreshControllers.forEach((controller) => stopAutoRefreshController(controller));
556
+
557
+ const deferToPaint = window.requestAnimationFrame
558
+ ? window.requestAnimationFrame.bind(window)
559
+ : (callback) => window.setTimeout(callback, 16);
560
+
561
+ deferToPaint(() => {
562
+ window.setTimeout(() => {
563
+ window.location.reload();
564
+ }, 120);
565
+ });
566
+ }
567
+
568
+ function startAutoRefreshReload(controller) {
569
+ if (autoRefreshReloadScheduled || controller.isLoading) {
570
+ return;
571
+ }
572
+
573
+ controller.isLoading = true;
574
+ stopAutoRefreshController(controller);
575
+ setAutoRefreshLoadingState(controller);
576
+ scheduleWindowReload();
577
+ }
578
+
579
+ function scheduleAutoRefreshTick(controller) {
580
+ if (autoRefreshReloadScheduled || controller.isLoading || controller.isPaused) {
581
+ return;
582
+ }
583
+
584
+ stopAutoRefreshController(controller);
585
+
586
+ const remainingMs = controller.deadline - Date.now();
587
+
588
+ if (remainingMs <= 0) {
589
+ startAutoRefreshReload(controller);
590
+ return;
591
+ }
592
+
593
+ const remainingSeconds = Math.ceil(remainingMs / 1000);
594
+ renderAutoRefreshCountdown(controller, remainingSeconds);
595
+
596
+ const nextDelay = remainingMs % 1000 || 1000;
597
+ controller.timeoutId = window.setTimeout(() => {
598
+ scheduleAutoRefreshTick(controller);
599
+ }, nextDelay);
600
+ }
601
+
602
+ function initializeAutoRefresh() {
603
+ document.querySelectorAll(selectors.autoRefresh).forEach((root) => {
604
+ if (!(root instanceof HTMLElement) || root._incAutoRefreshInitialized) {
605
+ return;
606
+ }
607
+
608
+ root._incAutoRefreshInitialized = true;
609
+
610
+ const refreshSeconds = parsePositiveInteger(root.getAttribute("data-inc-refresh-seconds"));
611
+
612
+ if (!refreshSeconds) {
613
+ return;
614
+ }
615
+
616
+ const controller = {
617
+ root,
618
+ parts: getAutoRefreshParts(root),
619
+ refreshLabel: root.getAttribute("data-inc-refresh-label") || "Refresh in",
620
+ loadingLabel: root.getAttribute("data-inc-refresh-loading-label") || "Refreshing",
621
+ pausedLabel: root.getAttribute("data-inc-refresh-paused-label") || "Paused at",
622
+ pauseActionLabel: root.getAttribute("data-inc-refresh-pause-action-label") || "Pause",
623
+ resumeActionLabel: root.getAttribute("data-inc-refresh-resume-action-label") || "Resume",
624
+ deadline: Date.now() + (refreshSeconds * 1000),
625
+ remainingMs: refreshSeconds * 1000,
626
+ timeoutId: 0,
627
+ isLoading: false,
628
+ isPaused: false,
629
+ };
630
+
631
+ root._incAutoRefreshController = controller;
632
+ autoRefreshControllers.push(controller);
633
+ scheduleAutoRefreshTick(controller);
634
+ });
635
+
636
+ if (!document._incAutoRefreshVisibilityBound && autoRefreshControllers.length) {
637
+ document._incAutoRefreshVisibilityBound = true;
638
+
639
+ document.addEventListener("visibilitychange", () => {
640
+ if (document.hidden || autoRefreshReloadScheduled) {
641
+ return;
642
+ }
643
+
644
+ autoRefreshControllers.forEach((controller) => {
645
+ if (controller.isLoading || controller.isPaused) {
646
+ return;
647
+ }
648
+
649
+ if ((controller.deadline - Date.now()) <= 0) {
650
+ startAutoRefreshReload(controller);
651
+ return;
652
+ }
653
+
654
+ scheduleAutoRefreshTick(controller);
655
+ });
656
+ });
657
+ }
658
+ }
659
+
383
660
  function trapFocus(event, container) {
384
661
  if (event.key !== "Tab") {
385
662
  return false;
@@ -470,6 +747,20 @@
470
747
 
471
748
  function attachEventHandlers() {
472
749
  document.addEventListener("click", (event) => {
750
+ const autoRefreshToggle = event.target.closest(selectors.autoRefreshToggle);
751
+
752
+ if (autoRefreshToggle) {
753
+ const autoRefreshRoot = autoRefreshToggle.closest(selectors.autoRefresh);
754
+ const controller = autoRefreshRoot?._incAutoRefreshController;
755
+
756
+ if (controller) {
757
+ event.preventDefault();
758
+ toggleAutoRefresh(controller);
759
+ }
760
+
761
+ return;
762
+ }
763
+
473
764
  const menuToggle = event.target.closest(selectors.menuToggle);
474
765
 
475
766
  if (menuToggle) {
@@ -686,6 +977,7 @@
686
977
  initializeMenus();
687
978
  initializeCollapses();
688
979
  initializeTabs();
980
+ initializeAutoRefresh();
689
981
  attachEventHandlers();
690
982
  }
691
983
 
@@ -2787,6 +2787,88 @@ dialog.inc-native-dialog.inc-native-dialog--drawer .inc-native-dialog__body {
2787
2787
  gap: 0.75rem;
2788
2788
  }
2789
2789
 
2790
+ .inc-auto-refresh {
2791
+ position: fixed;
2792
+ right: max(1rem, env(safe-area-inset-right, 0px) + 1rem);
2793
+ bottom: max(1rem, env(safe-area-inset-bottom, 0px) + 1rem);
2794
+ z-index: $inc-z-index-alerts - 1;
2795
+ display: inline-flex;
2796
+ align-items: center;
2797
+ gap: 0.625rem;
2798
+ padding: 0.5rem 0.75rem;
2799
+ border: 1px solid $inc-border-subtle;
2800
+ border-radius: 999px;
2801
+ background: rgba($inc-surface-primary, 0.96);
2802
+ color: $body-color;
2803
+ box-shadow: 0 0.75rem 1.5rem rgba($inc-surface-strong, 0.12);
2804
+ font-size: 0.75rem;
2805
+ line-height: 1.2;
2806
+ white-space: nowrap;
2807
+ backdrop-filter: blur(10px);
2808
+
2809
+ &--inline {
2810
+ position: static;
2811
+ right: auto;
2812
+ bottom: auto;
2813
+ z-index: auto;
2814
+ vertical-align: middle;
2815
+ }
2816
+
2817
+ &__countdown,
2818
+ &__status {
2819
+ display: inline-flex;
2820
+ align-items: center;
2821
+ gap: 0.5rem;
2822
+ min-height: 1rem;
2823
+ }
2824
+
2825
+ &__label,
2826
+ &__status-text {
2827
+ color: $text-muted;
2828
+ font-weight: 600;
2829
+ }
2830
+
2831
+ &__value {
2832
+ font-family: $font-family-monospace;
2833
+ font-variant-numeric: tabular-nums;
2834
+ font-weight: 600;
2835
+ color: $inc-surface-strong;
2836
+ }
2837
+
2838
+ &__spinner {
2839
+ display: inline-flex;
2840
+ align-items: center;
2841
+ color: $primary;
2842
+ }
2843
+
2844
+ &__toggle {
2845
+ flex: 0 0 auto;
2846
+
2847
+ &.inc-btn {
2848
+ min-height: 1.625rem;
2849
+ padding: 0.2rem 0.55rem;
2850
+ font-size: 0.6875rem;
2851
+ line-height: 1;
2852
+ }
2853
+ }
2854
+
2855
+ &__toggle-text {
2856
+ display: inline-block;
2857
+ min-width: 3.25rem;
2858
+ text-align: center;
2859
+ }
2860
+
2861
+ &.is-paused {
2862
+ border-color: rgba($warning, 0.24);
2863
+ box-shadow: 0 0.9rem 1.75rem rgba($warning, 0.1);
2864
+ }
2865
+
2866
+ &.is-loading {
2867
+ border-color: rgba($primary, 0.2);
2868
+ box-shadow: 0 0.9rem 1.75rem rgba($primary, 0.14);
2869
+ }
2870
+ }
2871
+
2790
2872
  .inc-progress,
2791
2873
  .inc-meter {
2792
2874
  width: 100%;