fix: toast handling, mobile breakpoint

`toast.css` and `index.js` - fixed toasts disappearing right after showing - simplified toast animation `index.css` - set 2ⁿᵈ mobile breakpoint at 640px

Alessandro committed Dec 4, 2024 at 19:37 UTC 7c2866ca614fff704b820e8f1ff6c9f50006320b
3 files changed +97 -51
webui/css/toast.css
+11 -4
@@ -9,8 +9,8 @@
9 display: none;
10 align-items: center;
11 z-index: 1000;
12 - transform: translateY(100%) scale(0.98);
13 - transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
12 + transform: translateY(100%);
13 + transition: transform 0.4s cubic-bezier(0.19, 0.86, 0.47, 1), background-color 0.3s ease;
14 will-change: transform;
15 }
16
@@ -43,11 +43,11 @@
43
44 #toast.show {
45 display: flex;
46 - transform: translateY(0) scale(1);
46 + transform: translateY(0);
47 }
48
49 #toast.hide {
50 - transform: translateY(100%) scale(0.98);
50 + transform: translateY(100%);
51 }
52
53 #toast.toast--success {
@@ -71,6 +71,13 @@
71 cursor: pointer;
72 font-size: 16px;
73 margin-left: 8px;
74 + opacity: 0.8;
75 + transition: opacity 0.2s ease;
76 +}
77 +
78 +.toast__close:hover,
79 +.toast__copy:hover {
80 + opacity: 1;
81 }
82
83 /* Animations */
webui/index.css
+2 -2
@@ -1535,7 +1535,7 @@ input:checked + .slider:before {
1535 }
1536
1537 /* Media Queries */
1538 -@media (max-width: 510px) {
1538 +@media (max-width: 640px) {
1539 .text-buttons-row {
1540 display: table;
1541 gap: 0.1rem !important;
@@ -1590,7 +1590,7 @@ input:checked + .slider:before {
1590 }
1591 }
1592
1593 -@media (max-width: 600px) {
1593 +@media (max-width: 640px) {
1594 #chat-input {
1595 min-height: 5.3rem;
1596 align-content: start;
webui/index.js
+84 -45
@@ -499,22 +499,35 @@ window.nudge = async function () {
499
500 window.restart = async function () {
501 try {
502 + // First try to initiate restart
503 const resp = await sendJsonData("/restart", {});
504 } catch (e) {
504 - //error expected here
505 - toast("Restarting...", "info", 0)
506 - while (true) {
505 + // Show restarting message
506 + toast("Restarting...", "info", 0);
507 +
508 + let retries = 0;
509 + const maxRetries = 60; // Maximum number of retries (15 seconds with 250ms interval)
510 +
511 + while (retries < maxRetries) {
512 try {
508 - // try health check until server is back up again
513 const resp = await sendJsonData("/health", {});
514 + // Server is back up, show success message
515 await new Promise(resolve => setTimeout(resolve, 250));
511 - toast("Restarted", "success", 2000)
516 + hideToast();
517 + await new Promise(resolve => setTimeout(resolve, 400));
518 + toast("Restarted", "success", 5000);
519 return;
520 } catch (e) {
514 - continue;
521 + // Server still down, keep waiting
522 + retries++;
523 + await new Promise(resolve => setTimeout(resolve, 250));
524 }
525 }
517 -
526 +
527 + // If we get here, restart failed or took too long
528 + hideToast();
529 + await new Promise(resolve => setTimeout(resolve, 400));
530 + toast("Restart timed out or failed", "error", 5000);
531 }
532 }
533
@@ -681,66 +694,92 @@ function removeClassFromElement(element, className) {
694
695 function toast(text, type = 'info', timeout = 5000) {
696 const toast = document.getElementById('toast');
684 - const isVisible = toast.style.display === 'flex';
697 + const isVisible = toast.classList.contains('show');
698
686 - // Update the toast content and type
687 - const title = type.charAt(0).toUpperCase() + type.slice(1);
688 - toast.querySelector('.toast__title').textContent = title;
689 - toast.querySelector('.toast__message').textContent = text;
690 - toast.className = `toast toast--${type}`;
699 + // Clear any existing timeout immediately
700 + if (toast.timeoutId) {
701 + clearTimeout(toast.timeoutId);
702 + toast.timeoutId = null;
703 + }
704
692 - // Show/hide copy button based on toast type
693 - const copyButton = toast.querySelector('.toast__copy');
694 - copyButton.style.display = type === 'error' ? 'inline-block' : 'none';
705 + // Function to update toast content and show it
706 + const updateAndShowToast = () => {
707 + // Update the toast content and type
708 + const title = type.charAt(0).toUpperCase() + type.slice(1);
709 + toast.querySelector('.toast__title').textContent = title;
710 + toast.querySelector('.toast__message').textContent = text;
711
696 - // Add the close button event listener
697 - const closeButton = toast.querySelector('.toast__close');
698 - closeButton.onclick = () => {
699 - hideToast();
700 - };
712 + // Remove old classes and add new ones
713 + toast.classList.remove('toast--success', 'toast--error', 'toast--info');
714 + toast.classList.add(`toast--${type}`);
715
702 - // Add the copy button event listener
703 - copyButton.onclick = () => {
704 - navigator.clipboard.writeText(text);
705 - copyButton.textContent = 'Copied!';
706 - setTimeout(() => {
707 - copyButton.textContent = 'Copy';
708 - }, 2000);
709 - };
716 + // Show/hide copy button based on toast type
717 + const copyButton = toast.querySelector('.toast__copy');
718 + copyButton.style.display = type === 'error' ? 'inline-block' : 'none';
719 +
720 + // Add the close button event listener
721 + const closeButton = document.querySelector('.toast__close');
722 + closeButton.onclick = () => {
723 + hideToast();
724 + };
725
711 - // Only trigger animation if toast is not already visible
712 - if (!isVisible) {
713 - // Remove any existing animation classes
714 - toast.classList.remove('show', 'hide');
726 + // Add the copy button event listener
727 + copyButton.onclick = () => {
728 + navigator.clipboard.writeText(text);
729 + copyButton.textContent = 'Copied!';
730 + setTimeout(() => {
731 + copyButton.textContent = 'Copy';
732 + }, 2000);
733 + };
734
716 - // Show the toast and trigger animation
735 + // Show the toast
736 toast.style.display = 'flex';
737 // Force a reflow to ensure the animation triggers
738 void toast.offsetWidth;
739 toast.classList.add('show');
721 - }
740
723 - // Clear any existing timeout
724 - if (toast.timeoutId)
725 - clearTimeout(toast.timeoutId);
741 + // Set timeout if specified
742 + if (timeout) {
743 + const minTimeout = Math.max(timeout, 5000);
744 + toast.timeoutId = setTimeout(() => {
745 + hideToast();
746 + }, minTimeout);
747 + }
748 + };
749
727 - // Automatically close the toast after specified timeout
728 - if (timeout)
729 - toast.timeoutId = setTimeout(() => {
730 - hideToast();
731 - }, timeout);
750 + if (isVisible) {
751 + // If a toast is visible, hide it first then show the new one
752 + toast.classList.remove('show');
753 + toast.classList.add('hide');
754 +
755 + // Wait for hide animation to complete before showing new toast
756 + setTimeout(() => {
757 + toast.classList.remove('hide');
758 + updateAndShowToast();
759 + }, 400); // Match this with CSS transition duration
760 + } else {
761 + // If no toast is visible, show the new one immediately
762 + updateAndShowToast();
763 + }
764 }
765
766 function hideToast() {
767 const toast = document.getElementById('toast');
768 +
769 + // Clear any existing timeout
770 + if (toast.timeoutId) {
771 + clearTimeout(toast.timeoutId);
772 + toast.timeoutId = null;
773 + }
774 +
775 toast.classList.remove('show');
776 toast.classList.add('hide');
777
739 - // Remove the element from display after animation completes
778 + // Wait for the hide animation to complete before removing from display
779 setTimeout(() => {
780 toast.style.display = 'none';
781 toast.classList.remove('hide');
743 - }, 300); // Match this with animation duration
782 + }, 400); // Match this with CSS transition duration
783 }
784
785 function scrollChanged(isAtBottom) {