diff --git a/README.md b/README.md index 67fcbad4..ccd7e615 100644 --- a/README.md +++ b/README.md @@ -444,6 +444,7 @@ docker compose up -d venv/bin/python3 -c "import sqlite3,sys; sqlite3.connect('db.sqlite3').backup(sqlite3.connect(sys.argv[1]))" "$b/db.sqlite3" && cp -p webvirtcloud/settings.py /etc/nginx/conf.d/webvirtcloud.conf "$b"/ && git rev-parse HEAD > "$b/commit" && + git checkout -- static/css/wvc-main.min.css dev/scss/wvc-main.scss && git checkout master && git pull ``` @@ -507,6 +508,7 @@ b=/root/webvirtcloud-backup-YYYYMMDD-HHMMSS supervisorctl stop webvirtcloud novncd cd /srv/webvirtcloud && ! supervisorctl status webvirtcloud novncd | grep RUNNING && +git checkout -- static/css/wvc-main.min.css dev/scss/wvc-main.scss && git checkout "$(cat "$b/commit")" && venv/bin/python3 -c "import os,sqlite3,sys; [os.remove(p) for p in ('db.sqlite3-journal','db.sqlite3-wal','db.sqlite3-shm') if os.path.exists(p)]; sqlite3.connect(sys.argv[1]).backup(sqlite3.connect('db.sqlite3'))" "$b/db.sqlite3" && cp "$b/settings.py" webvirtcloud/settings.py && @@ -552,6 +554,7 @@ A rollback leaves the clone on a detached HEAD at the restored commit; the updat - A running VM without a guest agent shows the IPv4 address the host learned by ARP (the lookup always failed before). - After a live migration the VM's NVRAM file is deleted on the source, when it is in libvirt's `/var/lib/libvirt/qemu/nvram` and the destination does not share that directory (a VM created there later with the same name would start with those UEFI variables). - gstfsd finds the guest's operating system the way libguestfs inspects it, so a root on LVM works, and it always answers (an error when it finds no or several operating systems). Update `/usr/local/bin/gstfsd` on every compute (see [Adding a Compute Node](#adding-a-compute-node), step 3). +- A theme picked on the Settings page rewrites two tracked files; the update and rollback steps put back the shipped theme. Pick yours again afterwards, also after Docker recreates the container. - Docker no longer publishes port 6080; the console goes through nginx on the panel's port (`WS_PUBLIC_PORT`, default 80). ## Usage diff --git a/appsettings/test_themes.py b/appsettings/test_themes.py new file mode 100644 index 00000000..40066979 --- /dev/null +++ b/appsettings/test_themes.py @@ -0,0 +1,33 @@ +"""The themes in dev/scss/wvc-themes compile with the pinned libsass, and the +shipped wvc-main.min.css is the default theme compiled from these sources.""" + +from pathlib import Path + +import sass +from django.conf import settings +from django.test import SimpleTestCase + +SCSS = Path(settings.BASE_DIR, "dev", "scss") + + +def compile_theme(theme): + # the imports the settings view compiles + return sass.compile( + string=f"@import '{SCSS}/wvc-themes/{theme}/variables';\n" + f"@import '{SCSS}/bootstrap-overrides.scss';\n" + f"@import '{SCSS}/wvc-themes/{theme}/bootswatch';", + output_style="compressed", + ) + + +class ThemeTests(SimpleTestCase): + def test_every_theme_compiles(self): + themes = sorted(p.name for p in (SCSS / "wvc-themes").iterdir() if p.is_dir()) + self.assertIn("zephyr", themes) + for theme in themes: + with self.subTest(theme=theme): + self.assertIn(".btn", compile_theme(theme)) + + def test_the_shipped_css_is_zephyr_from_these_sources(self): + shipped = Path(settings.BASE_DIR, "static", "css", "wvc-main.min.css").read_text() + self.assertEqual(shipped, compile_theme("zephyr")) diff --git a/dev/scss/bootstrap-overrides.scss b/dev/scss/bootstrap-overrides.scss index 94e6a0db..ba4a8243 100644 --- a/dev/scss/bootstrap-overrides.scss +++ b/dev/scss/bootstrap-overrides.scss @@ -1,6 +1,6 @@ /*! - * Bootstrap v5.3.3 (https://getbootstrap.com/) - * Copyright 2011-2024 The Bootstrap Authors + * Bootstrap v5.3.8 (https://getbootstrap.com/) + * Copyright 2011-2025 The Bootstrap Authors * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ diff --git a/dev/scss/bootstrap/_accordion.scss b/dev/scss/bootstrap/_accordion.scss index 17e5436e..e9f267fb 100755 --- a/dev/scss/bootstrap/_accordion.scss +++ b/dev/scss/bootstrap/_accordion.scss @@ -134,17 +134,12 @@ &:last-child { border-bottom: 0; } // stylelint-disable selector-max-class - > .accordion-header .accordion-button { - &, - &.collapsed { - @include border-radius(0); - } - } - // stylelint-enable selector-max-class - - > .accordion-collapse { + > .accordion-collapse, + > .accordion-header .accordion-button, + > .accordion-header .accordion-button.collapsed { @include border-radius(0); } + // stylelint-enable selector-max-class } } diff --git a/dev/scss/bootstrap/_button-group.scss b/dev/scss/bootstrap/_button-group.scss index 55ae3f65..78e12522 100755 --- a/dev/scss/bootstrap/_button-group.scss +++ b/dev/scss/bootstrap/_button-group.scss @@ -39,7 +39,7 @@ // Prevent double borders when buttons are next to each other > :not(.btn-check:first-child) + .btn, > .btn-group:not(:first-child) { - margin-left: calc(#{$btn-border-width} * -1); // stylelint-disable-line function-disallowed-list + margin-left: calc(-1 * #{$btn-border-width}); // stylelint-disable-line function-disallowed-list } // Reset rounded corners @@ -126,7 +126,7 @@ > .btn:not(:first-child), > .btn-group:not(:first-child) { - margin-top: calc(#{$btn-border-width} * -1); // stylelint-disable-line function-disallowed-list + margin-top: calc(-1 * #{$btn-border-width}); // stylelint-disable-line function-disallowed-list } // Reset rounded corners @@ -135,7 +135,12 @@ @include border-bottom-radius(0); } - > .btn ~ .btn, + // The top radius should be 0 if the button is: + // - the "third or more" child + // - the second child and the previous element isn't `.btn-check` (making it the first child visually) + // - part of a btn-group which isn't the first child + > .btn:nth-child(n + 3), + > :not(.btn-check) + .btn, > .btn-group:not(:first-child) > .btn { @include border-top-radius(0); } diff --git a/dev/scss/bootstrap/_card.scss b/dev/scss/bootstrap/_card.scss index d3535a98..dcebe6ac 100755 --- a/dev/scss/bootstrap/_card.scss +++ b/dev/scss/bootstrap/_card.scss @@ -193,8 +193,7 @@ // The child selector allows nested `.card` within `.card-group` // to display properly. > .card { - // Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4 - flex: 1 0 0%; + flex: 1 0 0; margin-bottom: 0; + .card { @@ -207,13 +206,13 @@ &:not(:last-child) { @include border-end-radius(0); - .card-img-top, - .card-header { + > .card-img-top, + > .card-header { // stylelint-disable-next-line property-disallowed-list border-top-right-radius: 0; } - .card-img-bottom, - .card-footer { + > .card-img-bottom, + > .card-footer { // stylelint-disable-next-line property-disallowed-list border-bottom-right-radius: 0; } @@ -222,13 +221,13 @@ &:not(:first-child) { @include border-start-radius(0); - .card-img-top, - .card-header { + > .card-img-top, + > .card-header { // stylelint-disable-next-line property-disallowed-list border-top-left-radius: 0; } - .card-img-bottom, - .card-footer { + > .card-img-bottom, + > .card-footer { // stylelint-disable-next-line property-disallowed-list border-bottom-left-radius: 0; } diff --git a/dev/scss/bootstrap/_carousel.scss b/dev/scss/bootstrap/_carousel.scss index 3a135220..5ebf6b15 100755 --- a/dev/scss/bootstrap/_carousel.scss +++ b/dev/scss/bootstrap/_carousel.scss @@ -99,6 +99,7 @@ color: $carousel-control-color; text-align: center; background: none; + filter: var(--#{$prefix}carousel-control-icon-filter); border: 0; opacity: $carousel-control-opacity; @include transition($carousel-control-transition); @@ -168,7 +169,7 @@ margin-left: $carousel-indicator-spacer; text-indent: -999px; cursor: pointer; - background-color: $carousel-indicator-active-bg; + background-color: var(--#{$prefix}carousel-indicator-active-bg); background-clip: padding-box; border: 0; // Use transparent borders to increase the hit area by 10px on top and bottom. @@ -195,42 +196,31 @@ left: (100% - $carousel-caption-width) * .5; padding-top: $carousel-caption-padding-y; padding-bottom: $carousel-caption-padding-y; - color: $carousel-caption-color; + color: var(--#{$prefix}carousel-caption-color); text-align: center; } // Dark mode carousel @mixin carousel-dark() { - .carousel-control-prev-icon, - .carousel-control-next-icon { - filter: $carousel-dark-control-icon-filter; - } - - .carousel-indicators [data-bs-target] { - background-color: $carousel-dark-indicator-active-bg; - } - - .carousel-caption { - color: $carousel-dark-caption-color; - } + --#{$prefix}carousel-indicator-active-bg: #{$carousel-indicator-active-bg-dark}; + --#{$prefix}carousel-caption-color: #{$carousel-caption-color-dark}; + --#{$prefix}carousel-control-icon-filter: #{$carousel-control-icon-filter-dark}; } .carousel-dark { @include carousel-dark(); } +:root, +[data-bs-theme="light"] { + --#{$prefix}carousel-indicator-active-bg: #{$carousel-indicator-active-bg}; + --#{$prefix}carousel-caption-color: #{$carousel-caption-color}; + --#{$prefix}carousel-control-icon-filter: #{$carousel-control-icon-filter}; +} + @if $enable-dark-mode { - @include color-mode(dark) { - @if $color-mode-type == "media-query" { - .carousel { - @include carousel-dark(); - } - } @else { - .carousel, - &.carousel { - @include carousel-dark(); - } - } + @include color-mode(dark, true) { + @include carousel-dark(); } } diff --git a/dev/scss/bootstrap/_close.scss b/dev/scss/bootstrap/_close.scss index 4d6e73c1..d53c96fb 100755 --- a/dev/scss/bootstrap/_close.scss +++ b/dev/scss/bootstrap/_close.scss @@ -12,7 +12,6 @@ --#{$prefix}btn-close-focus-shadow: #{$btn-close-focus-shadow}; --#{$prefix}btn-close-focus-opacity: #{$btn-close-focus-opacity}; --#{$prefix}btn-close-disabled-opacity: #{$btn-close-disabled-opacity}; - --#{$prefix}btn-close-white-filter: #{$btn-close-white-filter}; // scss-docs-end close-css-vars box-sizing: content-box; @@ -21,6 +20,7 @@ padding: $btn-close-padding-y $btn-close-padding-x; color: var(--#{$prefix}btn-close-color); background: transparent var(--#{$prefix}btn-close-bg) center / $btn-close-width auto no-repeat; // include transparent for button elements + filter: var(--#{$prefix}btn-close-filter); border: 0; // for button elements @include border-radius(); opacity: var(--#{$prefix}btn-close-opacity); @@ -47,17 +47,20 @@ } @mixin btn-close-white() { - filter: var(--#{$prefix}btn-close-white-filter); + --#{$prefix}btn-close-filter: #{$btn-close-filter-dark}; } .btn-close-white { @include btn-close-white(); } +:root, +[data-bs-theme="light"] { + --#{$prefix}btn-close-filter: #{$btn-close-filter}; +} + @if $enable-dark-mode { - @include color-mode(dark) { - .btn-close { - @include btn-close-white(); - } + @include color-mode(dark, true) { + @include btn-close-white(); } } diff --git a/dev/scss/bootstrap/_functions.scss b/dev/scss/bootstrap/_functions.scss index 90296586..59d431a1 100755 --- a/dev/scss/bootstrap/_functions.scss +++ b/dev/scss/bootstrap/_functions.scss @@ -157,7 +157,7 @@ $_luminance-list: .0008 .001 .0011 .0013 .0015 .0017 .002 .0022 .0025 .0027 .003 @each $color in $foregrounds { $contrast-ratio: contrast-ratio($background, $color); - @if $contrast-ratio > $min-contrast-ratio { + @if $contrast-ratio >= $min-contrast-ratio { @return $color; } @else if $contrast-ratio > $max-ratio { $max-ratio: $contrast-ratio; @@ -177,7 +177,7 @@ $_luminance-list: .0008 .001 .0011 .0013 .0015 .0017 .002 .0022 .0025 .0027 .003 @return if($l1 > $l2, divide($l1 + .05, $l2 + .05), divide($l2 + .05, $l1 + .05)); } -// Return WCAG2.1 relative luminance +// Return WCAG2.2 relative luminance // See https://www.w3.org/TR/WCAG/#dfn-relative-luminance // See https://www.w3.org/TR/WCAG/#dfn-contrast-ratio @function luminance($color) { diff --git a/dev/scss/bootstrap/_list-group.scss b/dev/scss/bootstrap/_list-group.scss index 455531ee..3bdff679 100755 --- a/dev/scss/bootstrap/_list-group.scss +++ b/dev/scss/bootstrap/_list-group.scss @@ -43,31 +43,6 @@ } } -// Interactive list items -// -// Use anchor or button elements instead of `li`s or `div`s to create interactive -// list items. Includes an extra `.active` modifier class for selected items. - -.list-group-item-action { - width: 100%; // For `