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) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-04-16 14:01:53 +00:00
parent 12f3e16b18
commit 5cbc36566b
3 changed files with 469 additions and 6 deletions

View File

@ -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;
}

View File

@ -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

View File

@ -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 });
})();