From 5cbc36566b66f444d80af774f0e90375b9626be7 Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Thu, 16 Apr 2026 14:01:53 +0000 Subject: [PATCH] animations: add UI animations across desk for a polished, alive feel - Dropdowns: smooth fade in/out via visibility+opacity transition - Modals/Dialogs: entrance with translateY+scale+opacity (0.5s) - Search dialog: fade in/out with slide, backdrop cleanup, input re-focus - Form sections: cascade entrance animation on load and tab switch - Form tabs: sliding active indicator via ::after scaleX - Collapse/expand: smooth max-height transition with JS-calculated height - Sidebar: hover padding shift, icon scale, active accent bar - List view: staggered row entrance from left - Workspace cards: slower entrance (0.7s), number value delayed fade-in - Widget z-index: open dropdown (200) above hovered neighbor (100) - Report summary: cascade card entrance - Link fields: awesomplete dropdown fade-in - Input focus: inset press effect - Buttons: shine sweep on hover, inset shadow on click - Chrome theme: tab indicator uses ::after instead of border-bottom - Observer fix: null-check parentNode for detached text nodes Co-Authored-By: Claude Opus 4.6 (1M context) --- jey_theme/public/css/shared.css | 364 +++++++++++++++++++++++++- jey_theme/public/css/theme_chrome.css | 5 +- jey_theme/public/js/jey_theme.js | 106 +++++++- 3 files changed, 469 insertions(+), 6 deletions(-) diff --git a/jey_theme/public/css/shared.css b/jey_theme/public/css/shared.css index 1f06b69..784dc2f 100644 --- a/jey_theme/public/css/shared.css +++ b/jey_theme/public/css/shared.css @@ -277,8 +277,353 @@ input.list-header-checkbox:checked::after { white-space: nowrap; } +/* ========================================================================== + Modal / Dialog — Smooth Entrance + ========================================================================== */ + +.modal.fade .modal-dialog { + transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1), + opacity 0.5s cubic-bezier(0.22, 1, 0.36, 1) !important; + transform: translateY(-24px) scale(0.97); + opacity: 0; +} + +.modal.show .modal-dialog { + transform: none; + opacity: 1; +} + + +/* ========================================================================== + Form Sections — Cascade Entrance + ========================================================================== */ + +@keyframes jey-section-in { + from { + opacity: 0; + transform: translateY(-12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.form-layout .form-section .section-head, +.form-layout .form-section .section-body { + animation: jey-section-in 0.7s cubic-bezier(0.22, 1, 0.36, 1) both; +} + +.form-layout .form-section:nth-child(1) .section-head, +.form-layout .form-section:nth-child(1) .section-body { animation-delay: 0.04s; } +.form-layout .form-section:nth-child(2) .section-head, +.form-layout .form-section:nth-child(2) .section-body { animation-delay: 0.10s; } +.form-layout .form-section:nth-child(3) .section-head, +.form-layout .form-section:nth-child(3) .section-body { animation-delay: 0.16s; } +.form-layout .form-section:nth-child(4) .section-head, +.form-layout .form-section:nth-child(4) .section-body { animation-delay: 0.22s; } +.form-layout .form-section:nth-child(5) .section-head, +.form-layout .form-section:nth-child(5) .section-body { animation-delay: 0.28s; } +.form-layout .form-section:nth-child(6) .section-head, +.form-layout .form-section:nth-child(6) .section-body { animation-delay: 0.34s; } +.form-layout .form-section:nth-child(7) .section-head, +.form-layout .form-section:nth-child(7) .section-body { animation-delay: 0.40s; } +.form-layout .form-section:nth-child(8) .section-head, +.form-layout .form-section:nth-child(8) .section-body { animation-delay: 0.46s; } +.form-layout .form-section:nth-child(9) .section-head, +.form-layout .form-section:nth-child(9) .section-body { animation-delay: 0.52s; } +.form-layout .form-section:nth-child(10) .section-head, +.form-layout .form-section:nth-child(10) .section-body { animation-delay: 0.58s; } +.form-layout .form-section:nth-child(11) .section-head, +.form-layout .form-section:nth-child(11) .section-body { animation-delay: 0.64s; } +.form-layout .form-section:nth-child(12) .section-head, +.form-layout .form-section:nth-child(12) .section-body { animation-delay: 0.70s; } + + +/* ========================================================================== + Form Tabs — Sliding Active Indicator + ========================================================================== */ + +.form-tabs .nav-link { + position: relative; + border-bottom: none !important; + transition: color 0.3s ease; +} + +.form-tabs .nav-link::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background: var(--text-color); + transform: scaleX(0); + transform-origin: center; + transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1); +} + +.form-tabs .nav-link.active::after { + transform: scaleX(1); +} + + +/* ========================================================================== + Dropdown Animations + ========================================================================== */ + +.dropdown-menu { + display: block !important; + visibility: hidden; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease, visibility 0.2s; +} + +.dropdown-menu.show { + visibility: visible; + opacity: 1; + pointer-events: auto; +} + +/* ========================================================================== + Sidebar — Hover Effects + ========================================================================== */ + +.body-sidebar .standard-sidebar-item .item-anchor { + transition: padding-left 0.4s cubic-bezier(0.22, 1, 0.36, 1), + background 0.4s ease; +} + +.body-sidebar .standard-sidebar-item .item-anchor:hover { + padding-left: calc(var(--padding-xs, 8px) + 4px); +} + +.body-sidebar .standard-sidebar-item .sidebar-item-icon { + transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1); +} + +.body-sidebar .standard-sidebar-item .item-anchor:hover .sidebar-item-icon { + transform: scale(1.15); +} + +/* Active item — subtle left accent bar */ +.body-sidebar .standard-sidebar-item.active-sidebar .item-anchor { + position: relative; +} + +.body-sidebar .standard-sidebar-item.active-sidebar .item-anchor::before { + content: ""; + position: absolute; + left: 0; + top: 20%; + bottom: 20%; + width: 3px; + border-radius: 0 3px 3px 0; + background: var(--primary, #2490ef); + opacity: 0.8; +} + + +/* ========================================================================== + List View — Row Entrance Animation + ========================================================================== */ + +@keyframes jey-list-row-in { + from { + opacity: 0; + transform: translateX(-16px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.frappe-list .result .list-row-container:not(:first-child) { + animation: jey-list-row-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; +} + +.frappe-list .result .list-row-container:nth-child(2) { animation-delay: 0.03s; } +.frappe-list .result .list-row-container:nth-child(3) { animation-delay: 0.05s; } +.frappe-list .result .list-row-container:nth-child(4) { animation-delay: 0.07s; } +.frappe-list .result .list-row-container:nth-child(5) { animation-delay: 0.09s; } +.frappe-list .result .list-row-container:nth-child(6) { animation-delay: 0.11s; } +.frappe-list .result .list-row-container:nth-child(7) { animation-delay: 0.13s; } +.frappe-list .result .list-row-container:nth-child(8) { animation-delay: 0.15s; } +.frappe-list .result .list-row-container:nth-child(9) { animation-delay: 0.17s; } +.frappe-list .result .list-row-container:nth-child(10) { animation-delay: 0.19s; } +.frappe-list .result .list-row-container:nth-child(11) { animation-delay: 0.21s; } +.frappe-list .result .list-row-container:nth-child(12) { animation-delay: 0.23s; } +.frappe-list .result .list-row-container:nth-child(13) { animation-delay: 0.25s; } +.frappe-list .result .list-row-container:nth-child(14) { animation-delay: 0.27s; } +.frappe-list .result .list-row-container:nth-child(15) { animation-delay: 0.29s; } +.frappe-list .result .list-row-container:nth-child(16) { animation-delay: 0.31s; } +.frappe-list .result .list-row-container:nth-child(17) { animation-delay: 0.33s; } +.frappe-list .result .list-row-container:nth-child(18) { animation-delay: 0.35s; } +.frappe-list .result .list-row-container:nth-child(19) { animation-delay: 0.37s; } +.frappe-list .result .list-row-container:nth-child(20) { animation-delay: 0.39s; } +.frappe-list .result .list-row-container:nth-child(21) { animation-delay: 0.41s; } + + +/* ========================================================================== + Search Dialog — Fade In / Out + ========================================================================== */ + +.modal:has(.cool-awesomebar-modal-footer) { + display: block !important; + visibility: hidden; + opacity: 0; + pointer-events: none; + transition: opacity 0.5s cubic-bezier(0.22, 1, 0.36, 1), + visibility 0.5s; +} + +.modal:has(.cool-awesomebar-modal-footer) .modal-dialog { + transform: translateY(-30px); + transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1); +} + +.modal.show:has(.cool-awesomebar-modal-footer) { + visibility: visible; + opacity: 1; + pointer-events: auto; +} + +.modal.show:has(.cool-awesomebar-modal-footer) .modal-dialog { + transform: none; +} + + +/* ========================================================================== + Report Summary — Card Cascade + ========================================================================== */ + +@keyframes jey-summary-in { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.report-summary .summary-item { + animation: jey-summary-in 0.7s cubic-bezier(0.22, 1, 0.36, 1) both; +} + +.report-summary .summary-item:nth-child(1) { animation-delay: 0.05s; } +.report-summary .summary-item:nth-child(2) { animation-delay: 0.10s; } +.report-summary .summary-item:nth-child(3) { animation-delay: 0.15s; } +.report-summary .summary-item:nth-child(4) { animation-delay: 0.20s; } +.report-summary .summary-item:nth-child(5) { animation-delay: 0.25s; } +.report-summary .summary-item:nth-child(6) { animation-delay: 0.30s; } + + +/* ========================================================================== + Form Section Collapse / Expand — Smooth + ========================================================================== */ + +.form-section .collapse-indicator { + transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1); + display: inline-block; +} + +.form-section .section-head.collapsed .collapse-indicator { + transform: rotate(-90deg); +} + +.form-section .section-head.collapsible + .section-body { + display: flex !important; + flex-wrap: wrap; + overflow: hidden; + transition: max-height 0.5s ease-in-out, + padding 0.5s ease-in-out; +} + +.form-section .section-head.collapsed + .section-body { + max-height: 0 !important; + padding-top: 0 !important; + padding-bottom: 0 !important; + transition: max-height 0.5s ease-in-out, + padding 0.5s ease-in-out; +} + + + +/* ========================================================================== + Link Field — Awesomplete Dropdown Fade + ========================================================================== */ + +.awesomplete > [role="listbox"] { + animation: jey-awesomplete-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; +} + +@keyframes jey-awesomplete-in { + from { opacity: 0; } + to { opacity: 1; } +} + + +/* ========================================================================== + Input Focus — Inset Press + ========================================================================== */ + +.form-control, +.frappe-control input, +.frappe-control textarea, +.frappe-control select { + transition: box-shadow 0.4s ease, border-color 0.4s ease; +} + +.form-control:focus, +.frappe-control input:focus, +.frappe-control textarea:focus, +.frappe-control select:focus { + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1) !important; + border-color: var(--gray-400, #c0c6cc) !important; + outline: none; +} + + +/* ========================================================================== + Buttons — Shine on Hover, Press on Click + ========================================================================== */ + +.btn { + position: relative; + overflow: hidden; + transition: box-shadow 0.4s ease, background-color 0.3s ease, color 0.3s ease; +} + +.btn::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(105deg, + transparent 35%, + rgba(255, 255, 255, 0.25) 45%, + rgba(255, 255, 255, 0.25) 55%, + transparent 65%); + transform: translateX(-110%); + transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1); + pointer-events: none; +} + +.btn:hover::after { + transform: translateX(110%); +} + +.btn:active { + box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.2) !important; +} + + /* Hide dropdown items now surfaced as sidebar buttons */ .dropdown-menu-item[onclick*="toggle_full_width"], +.dropdown-menu-item[onclick*="clear_cache"], .dropdown-menu-item[onclick*="ThemeSwitcher"], .dropdown-menu-item[data-name="website"], .dropdown-menu-item.jey-hidden-item { @@ -473,13 +818,23 @@ input.list-header-checkbox:checked::after { .ce-block .number-widget-box { opacity: 0; transform: translateY(24px) scale(0.97); - animation: jey-card-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) forwards; + animation: jey-card-in 0.7s cubic-bezier(0.22, 1, 0.36, 1) forwards; border-radius: var(--border-radius-lg, 12px); transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.35s ease; position: relative; } +/* Number card value — delayed fade-in after card entrance */ +@keyframes jey-number-in { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +.number-widget-box .widget-content .number { + animation: jey-number-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) 0.3s both; +} + /* Allow dropdown menus (card actions) to escape the widget box. Without this, .widget-control .dropdown-menu is clipped/hidden behind the widget because Popper renders it as a sibling inside the box. */ @@ -559,10 +914,13 @@ input.list-header-checkbox:checked::after { .ce-block .links-widget-box:hover, .ce-block .number-widget-box:hover, .ce-block .links-widget-box:focus-within, -.ce-block .number-widget-box:focus-within, +.ce-block .number-widget-box:focus-within { + z-index: 100; +} + .ce-block .links-widget-box:has(.dropdown-menu.show), .ce-block .number-widget-box:has(.dropdown-menu.show) { - z-index: 100; + z-index: 200; } diff --git a/jey_theme/public/css/theme_chrome.css b/jey_theme/public/css/theme_chrome.css index 0e4f6b2..4b54ee6 100644 --- a/jey_theme/public/css/theme_chrome.css +++ b/jey_theme/public/css/theme_chrome.css @@ -680,10 +680,13 @@ [data-jey-theme="chrome"] .form-tabs .nav-link.active { color: #333 !important; - border-bottom: 2px solid #777 !important; background: linear-gradient(180deg, rgba(255,255,255,0.2) 0%, transparent 100%) !important; } +[data-jey-theme="chrome"] .form-tabs .nav-link::after { + background: #777; +} + /* ========================================================================== Dropdowns — Chrome Panel diff --git a/jey_theme/public/js/jey_theme.js b/jey_theme/public/js/jey_theme.js index 722bf12..5bff602 100644 --- a/jey_theme/public/js/jey_theme.js +++ b/jey_theme/public/js/jey_theme.js @@ -1433,13 +1433,13 @@ var node = m.addedNodes[j]; if (node.nodeType === 1 && !SKIP_TAGS[node.tagName]) { processNode(node); - } else if (node.nodeType === 3 && !SKIP_TAGS[node.parentNode.tagName] && needsRebrand(node.nodeValue)) { + } else if (node.nodeType === 3 && node.parentNode && !SKIP_TAGS[node.parentNode.tagName] && needsRebrand(node.nodeValue)) { node.nodeValue = rebrandText(node.nodeValue); } } /* Handle direct text changes (e.g. frappe sets .textContent) */ if (m.type === "characterData" && m.target.nodeType === 3) { - if (!SKIP_TAGS[m.target.parentNode.tagName] && needsRebrand(m.target.nodeValue)) { + if (m.target.parentNode && !SKIP_TAGS[m.target.parentNode.tagName] && needsRebrand(m.target.nodeValue)) { m.target.nodeValue = rebrandText(m.target.nodeValue); } } @@ -1460,3 +1460,105 @@ init(); } })(); + + +/* ========================================================================== + Form Tabs — Re-trigger section cascade on tab switch + ========================================================================== */ +(function () { + document.addEventListener("click", function (e) { + var tab = e.target.closest(".form-tabs .nav-link"); + if (!tab) return; + + // Small delay so Frappe shows the tab content first + setTimeout(function () { + var sections = document.querySelectorAll(".form-layout .form-section .section-head, .form-layout .form-section .section-body"); + sections.forEach(function (s) { + s.style.animation = "none"; + // Force reflow then restart animation + void s.offsetHeight; + s.style.animation = ""; + }); + }, 20); + }); +})(); + + + + +/* ========================================================================== + Form Sections — Set exact max-height for smooth collapse animation + ========================================================================== */ +(function () { + document.addEventListener("click", function (e) { + var head = e.target.closest(".section-head.collapsible"); + if (!head) return; + var body = head.nextElementSibling; + if (!body || !body.classList.contains("section-body")) return; + + // Set exact max-height so transition has a real start/end value + body.style.maxHeight = body.scrollHeight + "px"; + // Force reflow — browser must register this value before Frappe toggles .collapsed + void body.offsetHeight; + }, true); +})(); + + +/* ========================================================================== + Search Modal — smooth close (backdrop cleanup) + ========================================================================== */ +// Bootstrap leaves the backdrop when we override display. +// Remove it manually when .show is removed. +(function () { + var searchModal = null; + var observer = new MutationObserver(function (mutations) { + for (var i = 0; i < mutations.length; i++) { + var m = mutations[i]; + if (m.attributeName === "class" && searchModal) { + if (!searchModal.classList.contains("show") && searchModal.dataset.jeyWasOpen) { + delete searchModal.dataset.jeyWasOpen; + // After transition ends, clean up backdrop + setTimeout(function () { + var bd = document.querySelector(".modal-backdrop"); + if (bd) bd.remove(); + document.body.classList.remove("modal-open"); + document.body.style.removeProperty("padding-right"); + }, 550); + } + if (searchModal.classList.contains("show")) { + searchModal.dataset.jeyWasOpen = "1"; + // Re-focus input after CSS transition so awesomplete opens + var input = searchModal.querySelector("#navbar-search"); + if (input) { + setTimeout(function () { + input.blur(); + input.focus(); + }, 550); + } + } + } + } + }); + + // Find search modal and start observing + var bodyObserver = new MutationObserver(function () { + if (searchModal) return; + var el = document.querySelector(".modal:has(.cool-awesomebar-modal-footer)"); + if (!el) return; + searchModal = el; + bodyObserver.disconnect(); + observer.observe(searchModal, { attributes: true, attributeFilter: ["class"] }); + // If .show is already present (race condition on first open), trigger focus now + if (searchModal.classList.contains("show")) { + searchModal.dataset.jeyWasOpen = "1"; + var input = searchModal.querySelector("#navbar-search"); + if (input) { + setTimeout(function () { + input.blur(); + input.focus(); + }, 550); + } + } + }); + bodyObserver.observe(document.body, { childList: true, subtree: true }); +})();