diff --git a/.stylelintrc.js b/.stylelintrc.js index 50820909fbe2..765545dc64f0 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -17,5 +17,6 @@ module.exports = { "number-max-precision": 5, "no-duplicate-selectors": null, "hue-degree-notation": "number", + "scss/operator-no-newline-after": null, }, }; diff --git a/core/src/main/resources/lib/form/bottomButtonBar.jelly b/core/src/main/resources/lib/form/bottomButtonBar.jelly index dab0895e29c3..2bab23e30266 100644 --- a/core/src/main/resources/lib/form/bottomButtonBar.jelly +++ b/core/src/main/resources/lib/form/bottomButtonBar.jelly @@ -28,9 +28,13 @@ THE SOFTWARE. Creates a button bar at the bottom of the page for things like "Submit". The actual buttons should be specified as the body of this tag. This area will always be visible at the bottom of the screen. + + + Hides the border of the bottom app bar unless its overlaying content. + -
+
diff --git a/src/main/js/components/dialogs/index.js b/src/main/js/components/dialogs/index.js index 60282e41008b..c8be1000a82a 100644 --- a/src/main/js/components/dialogs/index.js +++ b/src/main/js/components/dialogs/index.js @@ -13,6 +13,7 @@ let _defaults = { hideCloseButton: false, allowEmpty: false, submitButton: false, + preventCloseOnOutsideClick: false, }; let _typeClassMap = { @@ -39,11 +40,12 @@ Dialog.prototype.init = function () { this.dialog.style.minWidth = this.options.minWidth; document.body.appendChild(this.dialog); - if (this.options.title != null) { - const title = createElementFromHtml(`
`); - this.dialog.appendChild(title); - title.innerText = this.options.title; - } + // Append title element + const title = createElementFromHtml( + `
`, + ); + this.dialog.appendChild(title); + title.querySelector("span").innerText = this.options.title; if (this.dialogType === "modal") { if (this.options.content != null) { @@ -55,22 +57,24 @@ Dialog.prototype.init = function () { } if (this.options.hideCloseButton !== true) { const closeButton = createElementFromHtml(` - `); - this.dialog.appendChild(closeButton); + title.append(closeButton); closeButton.addEventListener("click", () => this.dialog.dispatchEvent(new Event("cancel")), ); } - this.dialog.addEventListener("click", function (e) { - if (e.target !== e.currentTarget) { - return; - } - this.dispatchEvent(new Event("cancel")); - }); + if (!this.options.preventCloseOnOutsideClick) { + this.dialog.addEventListener("click", function (e) { + if (e.target !== e.currentTarget) { + return; + } + this.dispatchEvent(new Event("cancel")); + }); + } this.ok = null; } else { this.form = null; diff --git a/src/main/scss/abstracts/_theme.scss b/src/main/scss/abstracts/_theme.scss index 0c15085fc00e..5fa0ace49499 100644 --- a/src/main/scss/abstracts/_theme.scss +++ b/src/main/scss/abstracts/_theme.scss @@ -77,6 +77,7 @@ $semantics: ( --header-height: 4.125rem; // App bar + --bottom-app-bar-padding: 0.875rem; --bottom-app-bar-shadow: color-mix( in sRGB, var(--text-color-secondary) 8%, @@ -201,8 +202,7 @@ $semantics: ( --dialog-background: var(--card-background); --dialog-box-shadow: var(--jenkins-border--subtle-shadow), 0 5px 25px rgb(0 0 10 / 0.025), - 0 0 1.5px color-mix(in sRGB, var(--black) 20%, transparent), - inset 0 0 0 1px rgb(255 255 255 / 0.05); + 0 0 1.5px color-mix(in sRGB, var(--black) 20%, transparent); ::backdrop { --dialog-backdrop-backdrop-filter: blur(0.25px); diff --git a/src/main/scss/components/_app-bar.scss b/src/main/scss/components/_app-bar.scss index b6921afddb6a..ec6b35a38994 100644 --- a/src/main/scss/components/_app-bar.scss +++ b/src/main/scss/components/_app-bar.scss @@ -125,25 +125,16 @@ text-overflow: ellipsis; } -$bottom-app-bar-padding: 0.875rem; - .jenkins-bottom-app-bar__shadow { - --semi-translucent: color-mix(in sRGB, var(--background) 75%, transparent); + --semi-translucent: color-mix(in sRGB, var(--background) 70%, transparent); position: sticky; height: calc( - 2.375rem + ($bottom-app-bar-padding * 2) + 1px + var(--jenkins-border-width) + 2.375rem + (var(--bottom-app-bar-padding) * 2) + 1px + + var(--jenkins-border-width) ); bottom: -1px; - margin-bottom: calc((2.375rem + $bottom-app-bar-padding) * -1); - backdrop-filter: blur(20px); - background: linear-gradient( - to right, - var(--background), - var(--semi-translucent) 3rem, - var(--semi-translucent) calc(100% - 3rem), - var(--background) - ); + margin-bottom: calc((2.375rem + var(--bottom-app-bar-padding)) * -1); border-top: var(--jenkins-border-width) solid var(--jenkins-border-color); z-index: 997; @@ -167,7 +158,33 @@ $bottom-app-bar-padding: 0.875rem; opacity: 0; } + &--borderless:not(&--stuck) { + border-top-color: transparent; + } + + &--borderless { + margin-top: calc( + (var(--bottom-app-bar-padding) + var(--jenkins-border-width)) * -1 + ); + } + &--stuck { + &::before { + content: ""; + position: absolute; + inset: 0; + backdrop-filter: blur(20px); + } + + background: linear-gradient( + to right, + var(--background), + var(--background) 3rem, + var(--semi-translucent), + var(--background) calc(100% - 3rem), + var(--background) + ); + &::after { opacity: 1 !important; } @@ -183,5 +200,5 @@ $bottom-app-bar-padding: 0.875rem; .bottom-sticker-inner { position: relative; - padding: $bottom-app-bar-padding 0; + padding: var(--bottom-app-bar-padding) 0; } diff --git a/src/main/scss/components/_dialogs.scss b/src/main/scss/components/_dialogs.scss index c955da7e767b..90b29a776d1e 100644 --- a/src/main/scss/components/_dialogs.scss +++ b/src/main/scss/components/_dialogs.scss @@ -2,6 +2,8 @@ $jenkins-dialog-padding: 1.25rem; .jenkins-dialog { --background: var(--dialog-background); + --section-padding: #{$jenkins-dialog-padding}; + --bottom-app-bar-padding: #{$jenkins-dialog-padding}; border-radius: 1rem; border: none; @@ -9,11 +11,12 @@ $jenkins-dialog-padding: 1.25rem; box-shadow: var(--dialog-box-shadow); animation: jenkins-dialog-animate-in 0.25s cubic-bezier(0, 0.68, 0.5, 1.5); overflow: hidden; - padding: $jenkins-dialog-padding 0 0 0; + padding: 0; display: flex; flex-direction: column; - gap: $jenkins-dialog-padding; - outline: none; + + // Thin highlight for dark themes to differentiate the dialog from its backdrop + outline: 1px solid rgb(255 255 255 / 0.05); &::backdrop { background: color-mix( @@ -26,13 +29,26 @@ $jenkins-dialog-padding: 1.25rem; } &__title { + display: flex; + gap: 1rem; + align-items: center; font-size: 1rem; font-weight: var(--font-bold-weight); - padding: 0 $jenkins-dialog-padding; + padding: $jenkins-dialog-padding; color: var(--text-color); overflow-wrap: anywhere; text-box: cap alphabetic; - margin-top: 0.5625rem; + + &__button { + padding: 0; + width: 2rem; + min-height: 2rem; + border-radius: 100%; + } + + &__close-button { + margin-left: auto !important; + } } &__contents { @@ -62,7 +78,7 @@ $jenkins-dialog-padding: 1.25rem; &__buttons { display: flex; - padding: 0 $jenkins-dialog-padding $jenkins-dialog-padding; + padding: $jenkins-dialog-padding; justify-content: right; flex-direction: row-reverse; gap: 1.25rem; @@ -80,16 +96,6 @@ $jenkins-dialog-padding: 1.25rem; margin: 0 0 1rem; } - &__close-button { - position: absolute; - top: $jenkins-dialog-padding; - right: $jenkins-dialog-padding; - padding: 0; - width: 2rem; - min-height: 2rem; - border-radius: 100%; - } - &[closing] { animation: jenkins-dialog-animate-out 0.1s linear; @@ -97,6 +103,12 @@ $jenkins-dialog-padding: 1.25rem; animation: jenkins-dialog-backdrop-animate-out 0.1s linear; } } + + .jenkins-bottom-app-bar__shadow { + margin-bottom: calc( + (2.375rem + calc(var(--bottom-app-bar-padding) * 2)) * -1 + ); + } } @keyframes jenkins-dialog-backdrop-animate-in { diff --git a/war/src/main/webapp/scripts/hudson-behavior.js b/war/src/main/webapp/scripts/hudson-behavior.js index f2614c66f6b8..21602c5975ec 100644 --- a/war/src/main/webapp/scripts/hudson-behavior.js +++ b/war/src/main/webapp/scripts/hudson-behavior.js @@ -1781,22 +1781,29 @@ function rowvgStartEachRow(recursive, f) { }, ); - window.addEventListener("load", function () { - // Add a class to the bottom bar when it's stuck to the bottom of the screen - const el = document.querySelector(".jenkins-bottom-app-bar__shadow"); - if (el) { + // Add a class to the bottom bar when it's stuck to the bottom of the screen + Behaviour.specify( + ".jenkins-bottom-app-bar__shadow", + "jenkins-bottom-app-bar__shadow", + 0, + function (el) { + const dialog = el.closest("dialog"); + const observer = new IntersectionObserver( ([e]) => e.target.classList.toggle( "jenkins-bottom-app-bar__shadow--stuck", e.intersectionRatio < 1, ), - { threshold: [1] }, + { + threshold: [1], + root: dialog || null, + }, ); observer.observe(el); - } - }); + }, + ); /** * Function that provides compatibility to the checkboxes without title on an f:entry