diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 08154ff9a1..2e1bd32759 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -5,7 +5,9 @@ ## Bug fixes -- When submitting bugfixes please show a before and after screenshot of the fix, and a description of what the fix does. +- PR descriptions for visual changes must include before/after screenshots and a short description of the fix. This applies to bug fixes, theme rewrites, and documentation gallery updates. +- Use comparable app states, theme options, and viewport sizes. Embed the images in the description; local-only screenshots do not meet this requirement. +- Changes with no visible result, such as workflow instructions, do not need screenshots. ## New theme option diff --git a/AGENTS.md b/AGENTS.md index 4a0e3ec871..dcddbc08e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,8 @@ affected app and inspect the result in a browser. ## Maintainer instructions +- Use `admin` / `admin` for disposable development app logins. If an app enforces an email address or a longer password, use `admin@example.com` and `adminadmin` as needed, and document the exception and login beside its startup URL. + - Do not push, publish, deploy, or trigger deployment workflows without an explicit instruction from the maintainer. Describing their usual release workflow does not authorize a push. Keep trial-run work local and reviewable. @@ -27,7 +29,8 @@ affected app and inspect the result in a browser. untested ideas. Keep run-specific evidence under ignored `dev/artifacts/`. - Always apply the `unslop` skill to writing. Keep PR titles and descriptions short, stating the user-visible fixes and relevant validation. Before/after - screenshot attachments are optional for bug-fix PRs, per the maintainer. + screenshots are required in PR descriptions for visual changes, including + bug fixes and theme rewrites. Show the affected UI in comparable states. The skill source is [cursor/plugins unslop](https://raw.githubusercontent.com/cursor/plugins/refs/heads/main/pstack/skills/unslop/SKILL.md). diff --git a/css/base/nginx-proxy-manager/nginx-proxy-manager-base.css b/css/base/nginx-proxy-manager/nginx-proxy-manager-base.css index b375dfea56..9049c74a59 100644 --- a/css/base/nginx-proxy-manager/nginx-proxy-manager-base.css +++ b/css/base/nginx-proxy-manager/nginx-proxy-manager-base.css @@ -1,347 +1,375 @@ -/* dP dP dP */ -/* 88 88 88 */ -/* d8888P 88d888b. .d8888b. 88d8b.d8b. .d8888b. 88d888b. .d8888b. 88d888b. 88 .dP */ -/* 88 88' `88 88ooood8 88'`88'`88 88ooood8 88' `88 88' `88 88' `88 88888" */ -/* 88 88 88 88. ... 88 88 88 88. ... 88. .88 88. .88 88 88 `8b. */ -/* dP dP dP `88888P' dP dP dP `88888P' 88 88Y888P' `88888P8 dP dP `YP */ -/* 88 */ -/* dP */ - -/* Made by @gilbN */ -/* https://github.com/gilbN/theme.park */ - +/* Nginx Proxy Manager's React / Tabler UI. */ @import url("/css/defaults/placeholders.css"); @import url("/css/defaults/transparent.css"); +/* NPM changes its native palette on the root and body. Both modes use the + selected theme.park palette; status colors retain their Tabler meanings. */ +:root, +:root[data-bs-theme], +body[data-theme] { + --tblr-body-color: var(--text); + --tblr-heading-color: var(--text-hover); + --tblr-emphasis-color: var(--text-hover); + --tblr-secondary: var(--text-muted); + --tblr-secondary-color: var(--text-muted); + --tblr-secondary-bg: var(--transparency-dark-15); + --tblr-tertiary-bg: var(--transparency-dark-10); + --tblr-tertiary-color: var(--text-muted); + --tblr-muted: var(--text-muted); + --tblr-icon-color: var(--text-muted); + --tblr-primary: rgb(var(--accent-color)); + --tblr-primary-rgb: var(--accent-color); + --tblr-primary-fg: var(--button-text); + --tblr-primary-darken: var(--button-color-hover); + --tblr-link-color: var(--link-color); + --tblr-link-hover-color: var(--link-color-hover); + --tblr-bg-surface: var(--transparency-dark-25); + --tblr-bg-surface-primary: var(--transparency-dark-25); + --tblr-bg-surface-secondary: var(--transparency-dark-15); + --tblr-bg-surface-tertiary: var(--transparency-dark-10); + --tblr-bg-forms: var(--transparency-dark-15); + --tblr-bg-surface-dark: var(--transparency-dark-25); + --tblr-border-color: color-mix(in srgb, var(--text) 22%, transparent); + --tblr-border-color-translucent: var(--tblr-border-color); + --tblr-border-dark-color: var(--tblr-border-color); + --tblr-border-active-color: rgb(var(--accent-color)); + --tblr-active-bg: rgba(var(--accent-color), .15); + --tblr-disabled-bg: var(--transparency-dark-10); + --tblr-disabled-color: color-mix(in srgb, var(--text) 45%, transparent); + --tblr-code-color: var(--text); + --tblr-code-bg: var(--transparency-dark-25); +} + body { background: var(--main-bg-color); - background-repeat: repeat, no-repeat; - background-attachment: fixed, fixed; - background-position: center center, center center; - background-size: auto, cover; - -webkit-background-size: auto, cover; - -moz-background-size: auto, cover; - -o-background-size: auto, cover; + background-attachment: fixed; color: var(--text); } -.header, .footer { - background: var(--transparency-dark-25); - color: var(--text); +::selection { + background: rgb(var(--accent-color)); + color: var(--label-text-color); } -.modal-content { - background: var(--modal-bg-color); - background-repeat: repeat, no-repeat; - background-attachment: fixed, fixed; - background-position: center center, center center; - background-size: auto, cover; - -webkit-background-size: auto, cover; - -moz-background-size: auto, cover; - -o-background-size: auto, cover; - color: var(--text); - border: none; +a:not(.btn, .badge, .dropdown-item), +.navbar .nav-link { + color: var(--link-color); } -.modal-header { - background: var(--modal-header-color); - background-repeat: repeat, no-repeat; - background-attachment: fixed, fixed; - background-position: center center, center center; - background-size: auto, cover; - -webkit-background-size: auto, cover; - -moz-background-size: auto, cover; - -o-background-size: auto, cover; - border-bottom: 1px solid var(--transparency-light-10); - color: var(--text-hover) +a:not(.btn, .badge, .dropdown-item):is(:hover, :focus-visible), +.navbar .nav-link:is(:hover, :focus-visible) { + color: var(--link-color-hover); } -.modal-footer { - background: var(--modal-footer-color); - background-repeat: repeat, no-repeat; - background-attachment: fixed, fixed; - background-position: center center, center center; - background-size: auto, cover; - -webkit-background-size: auto, cover; - -moz-background-size: auto, cover; - -o-background-size: auto, cover; - border-top: 1px solid var(--transparency-light-10); +/* Tabler marks link-secondary colors !important, including on hover. */ +.footer a.link-secondary { + color: var(--link-color) !important; } -/* NAVIGATION */ -.navbar-light .navbar-brand { - color: rgb(var(--accent-color)) +.footer a.link-secondary:is(:hover, :focus-visible) { + color: var(--link-color-hover) !important; } -.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus { - color: var(--accent-color-hover); +.navbar, +.footer { + background: var(--transparency-dark-25); + color: var(--text); } -.nav-tabs { - color: var(--link-color); +.navbar { + --tblr-navbar-color: var(--link-color); + --tblr-navbar-hover-color: var(--link-color-hover); + --tblr-navbar-active-color: var(--text-hover); + --tblr-navbar-brand-color: var(--text-hover); + --tblr-navbar-brand-hover-color: var(--link-color-hover); } -.nav-tabs .nav-link:hover:not(.disabled) { - border-color: var(--accent-color-hover); - color: var(--accent-color-hover); + +.navbar-toggler-icon { + background-image: linear-gradient(var(--text), var(--text)), + linear-gradient(var(--text), var(--text)), linear-gradient(var(--text), var(--text)); + background-size: 75% 2px; + background-position: center 25%, center 50%, center 75%; + background-repeat: no-repeat; } -.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link { - color: var(--accent-color-hover); - background-color: transparent; - border-color: var(--accent-color-hover) var(--accent-color-hover) var(--accent-color-hover); +.nav-tabs { + --tblr-nav-tabs-link-active-bg: var(--transparency-dark-15); + --tblr-nav-tabs-link-active-color: var(--text-hover); + --tblr-nav-tabs-link-active-border-color: rgb(var(--accent-color)); + background: transparent; } -.navbar-light .navbar-toggler-icon { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +.nav-link { + color: var(--text-muted); } -.navbar-light .navbar-toggler { - color: rgba(0,0,0,0.5); - border-color: var(--transparency-light-10); +.nav-tabs .nav-link:is(:hover, :focus-visible) { + color: var(--link-color-hover); + border-color: var(--accent-color-hover); } -.nav-tabs { - border-bottom: 1px solid var(--transparency-light-10); +.card, +.table { + color: var(--text); } +.table { + --tblr-table-hover-bg: var(--transparency-dark-10); + --tblr-table-hover-color: var(--text-hover); + --tblr-table-striped-bg: var(--transparency-dark-10); +} -/* DROPDOWNS AND SELECTS */ +.table thead th { + background: var(--transparency-dark-15); + color: var(--text-muted); +} + +/* An opaque menu must cover the table or dialog behind it. */ .dropdown-menu { + --tblr-dropdown-link-hover-bg: var(--transparency-dark-15); + --tblr-dropdown-link-hover-color: var(--text-hover); + --tblr-dropdown-header-color: var(--text-muted); background: var(--drop-down-menu-bg); color: var(--text); - border: 1px solid var(--transparency-light-10); } -.dropdown-item, -.card-options a { + +.modal-content { + background: var(--modal-bg-color); color: var(--text); + border-color: var(--tblr-border-color); } -.dropdown-item:hover, .dropdown-item:focus, -.card-options a:hover { + +.modal-header { + background: var(--modal-header-color); color: var(--text-hover); - text-decoration: none; - background-color: var(--transparency-dark-10); } -.dropdown-header { - color: var(--text-hover); +.modal-footer { + background: var(--modal-footer-color); } -.dropdown-icon { - color: rgb(var(--accent-color)); +.modal-header, +.modal-footer, +.modal-content { + background-attachment: fixed; } -.dropdown-divider { - border-top: 1px solid var(--transparency-light-10); +/* The editor's nested card must not darken or hide the modal body palette. */ +.modal-body > .card, +.modal-body > form > .card { + background: transparent; } -/* TEXT */ -p { +.btn-close { color: var(--text); -} -.text-default { - color: var(--text) !important; + filter: none; + opacity: .8; } -.text-muted, -.custom-switch-description, -.text-gray, -.text-secondary { - color: var(--text-muted) !important; +.btn-close:hover { + color: var(--text-hover); + opacity: 1; } -.text-blue { - color: rgb(var(--accent-color)) !important; +.btn { + --tblr-btn-focus-shadow-rgb: var(--accent-color); +} + +/* Save/Add are lime in NPM, while danger/warning/success actions keep theirs. */ +.btn-primary:not(:where(.btn-red, .btn-orange, .btn-danger, .btn-warning, .btn-success, .btn-info)), +.btn-lime, +.btn.bg-lime { + --tblr-btn-bg: var(--button-color); + --tblr-btn-color: var(--button-text); + --tblr-btn-hover-bg: var(--button-color-hover); + --tblr-btn-hover-color: var(--button-text-hover); + --tblr-btn-active-bg: var(--button-color-hover); + --tblr-btn-active-color: var(--button-text-hover); + --tblr-btn-disabled-bg: var(--button-color); + --tblr-btn-disabled-color: var(--button-text); + background-color: var(--tblr-btn-bg) !important; + color: var(--tblr-btn-color); +} + +.btn-primary:not(:where(.btn-red, .btn-orange, .btn-danger, .btn-warning, .btn-success, .btn-info)):hover, +.btn-lime:hover, +.btn.bg-lime:hover, +.btn-primary:not(:where(.btn-red, .btn-orange, .btn-danger, .btn-warning, .btn-success, .btn-info)):active, +.btn-lime:active, +.btn.bg-lime:active { + background-color: var(--button-color-hover) !important; + color: var(--button-text-hover); } -/* LINKS */ -a,.footer a:not(.btn) { - color: var(--link-color); -} -a:hover, a:focus, .footer a:not(.btn):hover { - color: var(--link-color-hover); +.btn.btn-ghost-dark, +.btn.btn-ghost-light, +.btn.btn-action { + --tblr-btn-bg: transparent; + --tblr-btn-color: var(--text-muted); + --tblr-btn-hover-color: var(--text-hover); + --tblr-btn-hover-bg: var(--transparency-dark-15); + /* NPM's locale and appearance modules set their icon colors !important. */ + color: var(--text-muted) !important; } -/* CARDS */ -.card { - background: var(--transparency-dark-25); +.btn.btn-ghost-dark:hover, +.btn.btn-ghost-light:hover, +.btn.btn-action:hover { + color: var(--text-hover) !important; } -.card-status[class*="bg-"] { - background: rgb(var(--accent-color)) !important; +:where(a, button, input, select, textarea):focus-visible { + outline: 2px solid rgb(var(--accent-color)); + outline-offset: 2px; } - -/* FORMS AND INPUTS */ - -.form-control, -.selectize-input, .selectize-control.single .selectize-input.input-active, -.selectize-control.single { - color: var(--text); - background-color: var(--transparency-dark-15); - border: 1px solid rgba(0,40,100,0.12); -} -.form-control:focus, -.selectize-input:focus, -.selectize-input.focus, -.selectize-input.full { +.form-control:focus:not(.is-invalid):not(.is-valid), +.form-select:focus:not(.is-invalid):not(.is-valid) { color: var(--text-hover); background-color: var(--transparency-dark-25); border-color: rgb(var(--accent-color)); - outline: 0; - box-shadow: 0 0 0 2px rgb(var(--accent-color),.25); + box-shadow: 0 0 0 .2rem rgba(var(--accent-color), .2); } -.form-control:disabled, .form-control[readonly] { - background-color: var(--transparency-light-35); - opacity: 1; -} - -select:focus { - background: var(--drop-down-menu-bg) !important; - color: var(--text) !important; -} - -.selectize-dropdown { +select option { background: var(--drop-down-menu-bg); - border: 1px solid var(--transparency-light-10); -} - -.selectize-dropdown .active { color: var(--text); - background-color: var(--transparency-light-10); } -.selectize-control.single .selectize-input:after { - background: none; +.form-control:disabled, +.form-control[readonly], +.form-select:disabled { + color: var(--text-muted); + background-color: var(--tblr-disabled-bg); } -.selectize-dropdown, .selectize-input, .selectize-input input { - color: var(--text); +.form-check-input { + border-color: color-mix(in srgb, var(--text) 45%, transparent); } -.selectize-control.multi .selectize-input>div { - background: rgb(var(--accent-color)); - color: var(--label-text-color); +.form-check-input:checked { + background-color: var(--button-color); } -.selectize-dropdown .create { - color: var(--text); -} -.selectize-dropdown .active.create { - color: var(--text-hover); +.form-switch .form-check-input { + background-image: radial-gradient(circle, var(--text) 0 40%, transparent 45%); } -.custom-file-label { - color: var(--text-muted); - background: var(--transparency-dark-15); - border: 1px solid var(--transparency-light-10); +.form-switch .form-check-input:checked { + background-image: radial-gradient(circle, var(--button-text) 0 40%, transparent 45%); } -.custom-file-label::after { - color: var(--button-text); - background-color: var(--button-color); - border-left: 1px solid var(--transparency-light-10); +.react-select-container .react-select__control { + background: var(--tblr-bg-forms); + color: var(--text); + border-color: var(--tblr-border-color); } -.form-fieldset { - background: var(--transparency-dark-25); - border: 1px solid var(--transparency-light-10); +.react-select-container .react-select__control.react-select__control--is-focused, +.react-select-container .react-select__control:hover { + border-color: rgb(var(--accent-color)); + box-shadow: 0 0 0 1px rgb(var(--accent-color)); } -/* BUTTONS */ - -[class*="btn-"]:not(.btn-list), -.card-options a:not(.dropdown-item.add-item), -.input-group-text { - color: var(--button-text); - background-color: var(--button-color); - border-color: var(--button-color); +.react-select-container .react-select__menu { + background: var(--drop-down-menu-bg); + color: var(--text); + border: 1px solid var(--tblr-border-color); } -[class*="btn-"]:hover:not(.btn-list), -.card-options a:hover:not(.dropdown-item.add-item), -[class*="btn-"]:not(:disabled):not(.disabled):not(.btn-list):active, [class*="btn-"]:not(:disabled):not(.disabled):not(.btn-list).active, .show>[class*="btn-"].dropdown-toggle, -[class*="btn-"].disabled,[class*="btn-"]:disabled { - color: var(--button-text-hover); - background-color: var(--button-color-hover); - border-color: var(--button-color-hover); +.react-select-container .react-select__placeholder, +.react-select-container .react-select__indicator { + color: var(--text-muted); } -[class*="btn-"]:focus, [class*="btn-"].focus { - box-shadow: 0 0 0 2px rgb(var(--accent-color), .5); -} -.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus, .show>.btn-secondary.dropdown-toggle:focus { - box-shadow: 0 0 0 2px rgb(var(--accent-color), .5); +.react-select-container .react-select__indicator:hover { + color: var(--text-hover); } -.custom-switch-input:checked ~ .custom-switch-indicator { - background: var(--button-color); +.react-select-container .react-select__indicator-separator { + background: var(--tblr-border-color); } -.custom-switch-input:focus ~ .custom-switch-indicator { - box-shadow: 0 0 0 2px rgba(var(--accent-color),0.25); - border-color: var(--button-color) +.react-select-container .react-select__control .react-select__multi-value { + background: rgb(var(--accent-color)); + color: var(--label-text-color); } -.custom-switch-description { - margin-left: .5rem; - color: #6e7687; - transition: .3s color +.react-select-container .react-select__control .react-select__multi-value .react-select__multi-value__label { + color: var(--label-text-color) !important; } -.custom-switch-input:checked ~ .custom-switch-description { - color: #495057 +.react-select-container .react-select__multi-value__remove { + color: var(--label-text-color); } -/* TABLES */ -.table th, .text-wrap table th { - color: var(--text-hover); +.react-select-container .react-select__multi-value__remove:hover { + color: var(--label-text-color); + background: var(--accent-color-hover); +} + +/* The editor supplies a dark syntax palette even in NPM's light mode. */ +.w-tc-editor[data-color-mode] { + --color-fg-default: var(--text); + --color-canvas-subtle: var(--transparency-dark-25); + --color-prettylights-syntax-comment: var(--text-muted); + --color-prettylights-syntax-entity-tag: var(--link-color-hover); + --color-prettylights-syntax-entity: var(--text-hover); + --color-prettylights-syntax-sublimelinter-gutter-mark: var(--text); + --color-prettylights-syntax-constant: var(--link-color-hover); + --color-prettylights-syntax-string: var(--link-color-hover); + --color-prettylights-syntax-string-regexp: var(--link-color-hover); + --color-prettylights-syntax-keyword: var(--text-hover); + --color-prettylights-syntax-markup-bold: var(--text-hover); + color: var(--text); + border: 1px solid var(--tblr-border-color); } -.table th, .text-wrap table th, .table td, .text-wrap table td { - border-top: 1px solid var(--transparency-light-10); +.Toastify__toast { + background: var(--drop-down-menu-bg); + color: var(--text); } -.table thead th, .text-wrap table thead th { - border-bottom: 1px solid rgb(var(--accent-color)); +.Toastify__close-button { + color: var(--text); } -/* OTHER */ - -.loader { - color: rgb(var(--accent-color)); +/* Floating labels replace placeholders; the shared placeholder import must + not paint a second label beneath them. */ +.form-floating > .form-control::placeholder, +.form-floating > .form-control:focus::placeholder { + color: transparent !important; } -.tag { - color: var(--label-text-color); - background-color: rgb(var(--accent-color)); +.form-check-input.bg-lime:checked { + background-color: var(--button-color) !important; } -.tag[class*="hover-"]:hover, [class*="hover-"]:active, [class*="hover-"]:focus { - background-color: var(--accent-color-hover); - color: var(--label-text-color); +.w-tc-editor pre, +.w-tc-editor code { + color: var(--text); + background: transparent; } -[class*="tag-"] { - background-color: var(--accent-color-hover); - color: var(--label-text-color); +.form-control::file-selector-button { + color: var(--button-text); + background: var(--button-color); } -.icon { - color: var(--text) !important; +.form-control:hover:not(:disabled):not([readonly])::file-selector-button { + color: var(--button-text-hover); + background: var(--button-color-hover); } -.close { - color: var(--text); - text-shadow: 0 1px 0 var(--text); -} -.close:hover, .close:focus { - color: var(--text-hover); +.form-floating > label, +.form-floating > :disabled ~ label { + color: var(--text-muted); } -pre { - color: var(--text); - background-color: var(--transparency-dark-15); - - text-shadow: 0 1px transparent; -} \ No newline at end of file +/* NPM renders a Tabler toast inside React Toastify's transparent wrapper. */ +.Toastify__toast .toast { + --tblr-toast-color: var(--text); + --tblr-toast-header-color: var(--text-hover); + --tblr-toast-header-bg: var(--transparency-dark-15); + background: var(--drop-down-menu-bg); +} diff --git a/dev/README.md b/dev/README.md index 3aa06b1d1c..8e884d66cf 100644 --- a/dev/README.md +++ b/dev/README.md @@ -25,6 +25,17 @@ The [Pi-hole setup](pihole/README.md) seeds synthetic network devices and uses nginx injection. Its notes explain the Network page's dependency on opaque CSS colors and how to reproduce a native stylesheet replacement. +The [Nginx Proxy Manager setup](nginx-proxy-manager/README.md) uses the official +startup script and includes browser checks for theme variables and UI states. + +## Development logins + +Use `admin` / `admin` for disposable app logins, as requested by the maintainer. +If the app enforces an email address, use `admin@example.com`. If it rejects the +short password, use `adminadmin` when accepted. Document any exception beside the +app URL so the maintainer does not have to ask for credentials. These defaults +apply to local test instances, not existing user accounts outside that setup. + ## Choose hosting and injection separately The CSS host serves stylesheets. The injection method makes the app load them. @@ -121,8 +132,10 @@ publishing changes. and run-specific results under ignored `dev/artifacts///`. Document reusable discoveries using the guidance below before finishing. 9. Keep PR descriptions short and apply the `unslop` skill. State the fixes and - relevant validation. Before/after screenshots can be attached but are not - mandatory for bug-fix PRs; keep local visual evidence for verification. + relevant validation. Include before/after screenshots in PR descriptions for visual + changes, including bug fixes and theme rewrites. Use the same app version, + theme, viewport and interaction state where possible. Keep the full local + evidence set for verification. 10. Leave changes local for review. Report which test services remain running and how to stop them. Stop only services created for the task. Delete saved configuration only when resetting the test instance. Never push or deploy without @@ -243,6 +256,20 @@ inline environment variable applies to that command only. Repeat the override when recreating the proxy to avoid reverting the review instance to its default theme. Record the selected theme and startup command in the task artifacts. +### Screenshots in pull requests + +Before/after screenshots are required for visual changes. Embed the images in +the PR description so reviewers can compare them without downloading files. +Use GitHub attachments or existing repository assets where suitable. If an +attachment is unavailable, commit only the selected review images under +`dev//screenshots/` and embed their immutable raw GitHub URLs. Keep full test +runs and intermediate captures under ignored `dev/artifacts/`. + +For a documentation gallery refresh, show the previous and updated gallery +images in the docs PR and label their app versions when known. Link the related +theme PR. Changes with no visible result, such as workflow instructions, do not +need screenshots. + ### Refresh documentation screenshots For a theme refresh, check the app's page in the separate `tp-docs` repository diff --git a/dev/nginx-proxy-manager/README.md b/dev/nginx-proxy-manager/README.md new file mode 100644 index 0000000000..875e12e79b --- /dev/null +++ b/dev/nginx-proxy-manager/README.md @@ -0,0 +1,186 @@ +# Nginx Proxy Manager development + +This setup runs NPM 2.15.1 with SQLite and disposable volumes. Only the admin +port is exposed, on loopback. The sample hosts forward to the local CSS server; +no public proxy ports or real certificates are needed. + +## Start and seed + +Run from the repository root. + +```sh +docker compose -f dev/nginx-proxy-manager/compose.yaml up -d +``` + +Open http://127.0.0.1:18084 and sign in with **admin@example.com / adminadmin**. +The maintainer's default is `admin` / `admin`. NPM's login form requires an +email address and at least eight password characters, so this setup uses the +documented fallback. `NPM_DEV_PASSWORD` can override the default password. +NPM applies the initial account variables only to an empty database. + +```sh +python3 dev/nginx-proxy-manager/seed.py +``` + +The seed script creates `media.example.test`, `downloads.example.test`, and a +standard user `reviewer@example.test` for permission-dialog checks, once. +Refresh Proxy Hosts after seeding. Data persists in the Compose volumes. + +## CSS injection + +The [NPM theme documentation](https://docs.theme-park.dev/themes/nginx-proxy-manager/) +uses an executable startup script, not the LinuxServer `DOCKER_MODS` variable. +Compose mounts the repository's executable script at `/etc/cont-init.d/99-themepark`. +In 2.15.1 this inserts base and theme-option links at the end of +`/app/frontend/index.html`'s head. No proxy injection is needed. + +The CSS host serves the current checkout at http://127.0.0.1:18867. +The browser must be able to reach that loopback address. This setup is for a +browser on the Docker host. Confirm requests for the base CSS, theme option, +`placeholders.css`, and `transparent.css` in the browser or CSS server logs. + +```sh +docker compose -f dev/nginx-proxy-manager/compose.yaml logs css +``` + +Edit the base CSS and reload to test. To change the injected theme, recreate +NPM, since the startup script does not replace links already present in HTML. +Container recreation restores the image's original HTML before injection. + +```sh +TP_THEME=nord docker compose -f dev/nginx-proxy-manager/compose.yaml up -d --force-recreate npm +``` + +Repeat the chosen `TP_THEME` override whenever recreating NPM. Compare native +styling by disabling both injected link elements in browser developer tools. +A full reload restores the injected styles. + +## UI migration and issue 707 + +[Issue 707](https://github.com/themepark-dev/theme.park/issues/707) was opened +on November 4, 2025. It reports mismatched white and dark-blue areas, without +an app version, browser version, named theme, or injection method. +[NPM 2.13.0](https://github.com/NginxProxyManager/nginx-proxy-manager/releases/tag/v2.13.0) +introduced React, updated Tabler, and native light/dark modes. Do not treat +2.13.0 as the reporter's confirmed version. + +The existing theme reproduces the mismatch on 2.15.1. In native light mode, +Proxy Hosts and Users tables have dark text on themed dark backgrounds. +The Add Proxy Host dialog has dark labels, white React Select controls and +switches, and a white tab strip. In native dark mode those controls and the +navigation retain NPM's blue-gray palette. The page and modal gradients load. +The modal contains a `.card` that applies its own text color and background, +so changing `.modal-content` alone cannot fix the labels. + +The rewrite fixes these theme compatibility defects. The Users table repeats the same contrast defect +outside the reported host screen. The rewrite also removes filled backgrounds that the old broad +`[class*="btn-"]` rule incorrectly applied to ghost buttons. + +Implementation notes: + +- NPM sets `data-bs-theme` on the root element, `data-theme` and a light/dark + class on the body, and stores its choice as `tabler-theme` in local storage. +- Tabler uses `--tblr-body-color`, `--tblr-bg-forms`, and surface, card, table, + border and button variables. Map them to theme.park colors in both modes. + Inspect component-local declarations before relying on root overrides. +- React Select exposes `.react-select__control`, `__menu`, `__option`, and + `__multi-value` classes. Old `.selectize-*` rules do not cover it. +- Switches use `.form-check-input`, headers use `.navbar`, and modal close + buttons use `.btn-close`. The old `.custom-switch-input`, `.header`, and + `.close` selectors matched nothing on the inspected host page and dialog. + Backwards compatibility was explicitly excluded by the maintainer. +- Preserve status colors and button variants. Use `background` for theme + variables that can contain gradients. Inspect the modal's nested card, + tabs, header and footer individually. + +The browser review covers login/setup, navigation, populated and empty host tables, +host editors and their SSL/custom-location/advanced tabs, access lists, +certificates, users/permissions, audit logs, settings, notifications and errors. +The core verification runs in both native modes at desktop and mobile sizes, +with Chromium and Firefox. It checks Nord, Aquamarine, Hotline, and +Catppuccin Latte. Certificate issuance and real proxy traffic are outside the +CSS checks. The gray text baked into NPM's login/setup logo remains unchanged. + +## Verify + +Install Playwright in a temporary virtual environment, outside the repository. + +```sh +python3 -m venv /tmp/npm-theme-tools +/tmp/npm-theme-tools/bin/pip install playwright +/tmp/npm-theme-tools/bin/playwright install chromium firefox +/tmp/npm-theme-tools/bin/python dev/nginx-proxy-manager/verify.py +/tmp/npm-theme-tools/bin/python dev/nginx-proxy-manager/verify_variables.py +``` + +`verify.py` saves screenshots and checks the loaded source CSS, imports, primary +button hover, domain selection, switch state, focus, disabled SSL controls, +editor, and independent modal backgrounds. Set `TP_THEME` to match the injected +theme. A run takes both browsers through light/dark mode and 1440x1000/390x844. +Inspect the screenshots as well as the results JSON. + +`verify_variables.py` uses a temporary browser palette with distinct values for +every general theme variable. It checks navigation, footer and dashboard links +with a real pointer and keyboard focus, buttons, labels, menus, and independent +radial/linear/solid modal sections. Reload removes that diagnostic palette. + +For the light community option: + +```sh +TP_THEME=catppuccin-latte TP_COMMUNITY_THEME=true docker compose -f dev/nginx-proxy-manager/compose.yaml up -d --force-recreate npm +TP_THEME=catppuccin-latte /tmp/npm-theme-tools/bin/python dev/nginx-proxy-manager/verify.py +``` + +Restore the default review theme after testing: + +```sh +docker compose -f dev/nginx-proxy-manager/compose.yaml up -d --force-recreate npm +``` + +Tabler's footer `.link-secondary` rules use `!important` in normal and hover +states. Mapping only `--tblr-link-hover-color` does not override them. Navigation +has separate local colors. Verify both components directly when changing links. +React Select's generated hover rule can also beat a control rule with equal +specificity. Its multi-value label has a four-class important selector in NPM. + +NPM renders `.toast` and `.toast-header` inside a transparent Toastify wrapper. +Style those inner elements to avoid native white headers and transparent bodies. +Floating labels replace placeholders, so the shared placeholder import must not +make both labels visible. The editor sets `data-color-mode="dark"` regardless of +NPM mode; map its syntax variables and the `pre`/`code` foreground as well as the +container background. Keep the textarea overlay transparent. + +## Stop or reset + +```sh +docker compose -f dev/nginx-proxy-manager/compose.yaml down +``` + +Add `--volumes` only to reset this disposable instance and delete its users, +hosts and certificates. Run-specific screenshots and findings belong in ignored +`dev/artifacts/nginx-proxy-manager/707/`. + +## Theme variable consumers + +| theme.park variable | NPM consumer | +| --- | --- | +| `--main-bg-color` | Document background, using the full background shorthand. | +| `--modal-bg-color` | Modal shell/body; the nested editor card stays transparent. | +| `--modal-header-color` | Modal header. | +| `--modal-footer-color` | Modal footer. | +| `--drop-down-menu-bg` | Dropdowns, React Select menus, notifications, native select options. | +| `--button-color`, `--button-text` | Primary actions, file upload buttons, checked switches. | +| `--button-color-hover`, `--button-text-hover` | Primary action and upload hover/active states. | +| `--accent-color` | Focus rings, selected tab borders, selection and domain-label backgrounds. Remains an RGB triplet. | +| `--accent-color-hover` | Tab hover borders and the remove action on domain labels. Remains a CSS color. | +| `--link-color`, `--link-color-hover` | Navigation, dashboard links, footer and ordinary anchors, including keyboard focus. | +| `--label-text-color` | Text selection and domain-label foregrounds. | +| `--text` | Body, table, form and editor text; neutral buttons. | +| `--text-hover` | Headings, focused inputs, close-button hover and menu action hover. | +| `--text-muted` | Secondary text, placeholders, floating labels and neutral icons. | + +Danger, warning and success actions and status badges keep their semantic colors. +Neutral Cancel buttons and ghost icon controls retain a separate style from +primary actions. App-specific specials such as `--arr-queue-color`, +`--plex-poster-unwatched`, `--petio-spinner`, `--gitea-color-primary-dark-4` and +`--overseerr-gradient` have no NPM consumers and are intentionally unused. diff --git a/dev/nginx-proxy-manager/compose.yaml b/dev/nginx-proxy-manager/compose.yaml new file mode 100644 index 0000000000..c0f94cc639 --- /dev/null +++ b/dev/nginx-proxy-manager/compose.yaml @@ -0,0 +1,28 @@ +name: themepark-npm-dev +services: + css: + image: python:3.12-alpine + working_dir: /site + command: ["python", "/serve.py"] + ports: ["127.0.0.1:18867:8000"] + volumes: + - ../../css:/site/css:ro + - ../../resources:/site/resources:ro + - ../serve.py:/serve.py:ro + npm: + image: jc21/nginx-proxy-manager:2.15.1 + environment: + INITIAL_ADMIN_EMAIL: admin@example.com + INITIAL_ADMIN_PASSWORD: ${NPM_DEV_PASSWORD:-adminadmin} + TP_DOMAIN: 127.0.0.1:18867 + TP_SCHEME: http + TP_THEME: ${TP_THEME:-aquamarine} + TP_COMMUNITY_THEME: ${TP_COMMUNITY_THEME:-false} + ports: ["127.0.0.1:18084:81"] + volumes: + - data:/data + - certificates:/etc/letsencrypt + - ../../docker-mods/nginx-proxy-manager/root/etc/cont-init.d/98-themepark:/etc/cont-init.d/99-themepark:ro +volumes: + data: + certificates: diff --git a/dev/nginx-proxy-manager/screenshots/README.md b/dev/nginx-proxy-manager/screenshots/README.md new file mode 100644 index 0000000000..be97411202 --- /dev/null +++ b/dev/nginx-proxy-manager/screenshots/README.md @@ -0,0 +1,12 @@ +# PR 734 screenshots + +Both captures show NPM 2.15.1, Aquamarine, native light mode, Chromium +151.0.7922.34, and a 1440x1000 viewport with synthetic hosts. + +`before.png` uses the base CSS from `968f7bed` in an isolated browser request +override. `after.png` loads the rewritten CSS through the mounted startup script. +The Add Proxy Host dialog is empty in both captures. The running review +instance remains on the rewritten stylesheet. + +These two images are embedded in the PR. Full test results and intermediate +screenshots remain under ignored `dev/artifacts/nginx-proxy-manager/707/`. diff --git a/dev/nginx-proxy-manager/screenshots/after.png b/dev/nginx-proxy-manager/screenshots/after.png new file mode 100644 index 0000000000..69be30c19e Binary files /dev/null and b/dev/nginx-proxy-manager/screenshots/after.png differ diff --git a/dev/nginx-proxy-manager/screenshots/before.png b/dev/nginx-proxy-manager/screenshots/before.png new file mode 100644 index 0000000000..d93e93f08b Binary files /dev/null and b/dev/nginx-proxy-manager/screenshots/before.png differ diff --git a/dev/nginx-proxy-manager/seed.py b/dev/nginx-proxy-manager/seed.py new file mode 100644 index 0000000000..933718d092 --- /dev/null +++ b/dev/nginx-proxy-manager/seed.py @@ -0,0 +1,38 @@ +"""Add two disposable proxy hosts to the local NPM review instance.""" +import json +import os +import urllib.request + +BASE = "http://127.0.0.1:18084/api" + + +def request(path, payload=None, token=None): + headers = {"Content-Type": "application/json"} + if token: + headers["Authorization"] = f"Bearer {token}" + data = None if payload is None else json.dumps(payload).encode() + with urllib.request.urlopen(urllib.request.Request(BASE + path, data, headers)) as response: + return json.load(response) + + +token = request("/tokens", {"identity": "admin@example.com", "secret": os.environ.get("NPM_DEV_PASSWORD", "adminadmin")})["token"] +existing = {domain for host in request("/nginx/proxy-hosts", token=token) for domain in host["domain_names"]} +for domain in ("media.example.test", "downloads.example.test"): + if domain not in existing: + request("/nginx/proxy-hosts", { + "domain_names": [domain], "forward_scheme": "http", + "forward_host": "css", "forward_port": 8000, + "access_list_id": 0, "certificate_id": 0, + "ssl_forced": False, "caching_enabled": False, + "block_exploits": False, "allow_websocket_upgrade": False, + "http2_support": False, "advanced_config": "", "locations": [], + "meta": {}, + }, token) + print(f"Created {domain}") + +if not any(user["email"] == "reviewer@example.test" for user in request("/users", token=token)): + request("/users", { + "name": "Theme Reviewer", "nickname": "Reviewer", + "email": "reviewer@example.test", "roles": [], "is_disabled": False, + }, token) + print("Created the review user for permission-dialog checks") diff --git a/dev/nginx-proxy-manager/verify.py b/dev/nginx-proxy-manager/verify.py new file mode 100644 index 0000000000..ff5c1efd0f --- /dev/null +++ b/dev/nginx-proxy-manager/verify.py @@ -0,0 +1,112 @@ +"""Check NPM's injected theme in Chromium/Firefox and save visual evidence. + +Requires Playwright and its chromium/firefox browsers. Run after Compose and seed.py. +TP_THEME must match the theme currently injected by the container. +""" +import hashlib +import json +import os +import re +from pathlib import Path +from playwright.sync_api import sync_playwright + +ROOT = Path(__file__).resolve().parents[2] +THEME = os.environ.get('TP_THEME', 'aquamarine') +OUT = ROOT / 'dev/artifacts/nginx-proxy-manager/707' / THEME +OUT.mkdir(parents=True, exist_ok=True) +URL = 'http://127.0.0.1:18084' +results = [] + + +def color(page, variable): + return page.evaluate('''variable => { + const probe = document.createElement('i'); + probe.style.color = `var(${variable})`; document.body.append(probe); + const value = getComputedStyle(probe).color; probe.remove(); return value; + }''', variable) + + +def style(locator, property): + value = locator.evaluate('(el,p)=>getComputedStyle(el).getPropertyValue(p)', property) + return re.sub(r'rgba\((\d+, \d+, \d+), 1\)', r'rgb(\1)', value) + + +with sync_playwright() as playwright: + for engine in ('chromium', 'firefox'): + browser = getattr(playwright, engine).launch() + for mode in ('light', 'dark'): + for width, height in ((1440, 1000), (390, 844)): + context = browser.new_context(viewport={'width': width, 'height': height}, color_scheme=mode) + page = context.new_page() + page.set_default_timeout(10000) + loaded = {} + page.on('response', lambda response: loaded.update({response.url.split('?')[0]: response.status}) if '/css/' in response.url else None) + prefix = f'{engine}-{mode}-{width}' + + def shot(name): + page.wait_for_timeout(250) + page.screenshot(path=str(OUT / f'{prefix}-{name}.png'), full_page=True) + + page.goto(URL) + page.locator('input[name=email]').wait_for() + shot('login') + page.locator('input[name=email]').fill('admin@example.com') + page.locator('input[name=password]').fill(os.environ.get('NPM_DEV_PASSWORD', 'adminadmin')) + page.get_by_role('button', name='Sign in', exact=True).click() + page.get_by_text('0 Redirection Hosts', exact=True).wait_for() + assert page.locator('html').get_attribute('data-bs-theme') == mode + page.goto(URL + '/nginx/proxy') + page.get_by_role('button', name='Add Proxy Host', exact=True).wait_for() + expected = color(page, '--text') + assert style(page.locator('.table tbody td').nth(2), 'color') == expected + assert page.evaluate('document.documentElement.scrollWidth <= innerWidth'), 'Page overflows horizontally' + shot('table') + add = page.get_by_role('button', name='Add Proxy Host', exact=True) + add.hover(); page.wait_for_timeout(250) + assert style(add, 'background-color') == color(page, '--button-color-hover') + assert style(add, 'color') == color(page, '--button-text-hover') + shot('hover') + add.click() + page.locator('.modal-content').wait_for() + assert style(page.locator('.modal-body .card'), 'background-color') == 'rgba(0, 0, 0, 0)' + assert style(page.locator('.modal-body .card'), 'color') == expected + for selector, variable in (('.modal-content','--modal-bg-color'),('.modal-header','--modal-header-color'),('.modal-footer','--modal-footer-color')): + actual = page.locator(selector).evaluate('''(el,variable)=>{ + const probe=document.createElement('i');probe.style.background=`var(${variable})`;el.append(probe); + const expected=getComputedStyle(probe);const actual=getComputedStyle(el); + const result={color:actual.backgroundColor===expected.backgroundColor,image:actual.backgroundImage===expected.backgroundImage};probe.remove();return result; + }''', variable) + assert all(actual.values()), (selector, actual) + domain = page.locator('#domainNames input[role=combobox]') + domain.fill('preview.example.test');page.keyboard.press('Enter') + assert page.locator('.react-select__multi-value').count() == 1 + page.locator('#cachingEnabled').check();page.wait_for_timeout(650) + assert style(page.locator('#cachingEnabled'), 'background-color') == color(page, '--button-color'), (page.locator('#cachingEnabled').evaluate('(x)=>x.outerHTML'), style(page.locator('#cachingEnabled'), 'background-color'),color(page, '--button-color')) + page.locator('#forwardHost').fill('css') + page.locator('#forwardPort').fill('8000') + page.locator('#forwardHost').focus();page.wait_for_timeout(250) + assert style(page.locator('#forwardHost'), 'border-top-color') == color(page, '--tblr-primary') + shot('dialog') + page.locator('.react-select__control').nth(1).click();page.wait_for_timeout(250) + assert style(page.locator('.react-select__control').nth(1), 'border-top-color') == color(page, '--tblr-primary') + shot('select');page.keyboard.press('Escape') + page.get_by_role('tab', name='SSL', exact=True).click() + assert page.locator('.modal input:disabled').count() > 0 + shot('ssl-disabled') + page.locator('.modal a[title=Settings]').click() + page.locator('.w-tc-editor textarea').fill('# local preview\nproxy_set_header X-Theme "preview";') + assert style(page.locator('.w-tc-editor code'), 'color') == expected + shot('editor') + page.locator('.modal .btn-close').click() + page.locator('.modal').wait_for(state='hidden') + # Compare actual downloaded source, not a browser-only style prototype. + base='http://127.0.0.1:18867/css/base/nginx-proxy-manager/nginx-proxy-manager-base.css' + assert hashlib.sha256(page.request.get(base).body()).digest() == hashlib.sha256((ROOT/'css/base/nginx-proxy-manager/nginx-proxy-manager-base.css').read_bytes()).digest() + for suffix in ('base/nginx-proxy-manager/nginx-proxy-manager-base.css','defaults/placeholders.css','defaults/transparent.css'): + assert loaded.get('http://127.0.0.1:18867/css/'+suffix) == 200, loaded + assert any(url.endswith('/'+THEME+'.css') and code==200 for url,code in loaded.items()), loaded + results.append({'browser':engine,'version':browser.version,'mode':mode,'viewport':[width,height],'theme':THEME,'checks':'passed'}) + print(prefix, 'passed', flush=True) + context.close() + browser.close() +(OUT/'results.json').write_text(json.dumps(results,indent=2)+'\n') diff --git a/dev/nginx-proxy-manager/verify_variables.py b/dev/nginx-proxy-manager/verify_variables.py new file mode 100644 index 0000000000..37a99adaa6 --- /dev/null +++ b/dev/nginx-proxy-manager/verify_variables.py @@ -0,0 +1,103 @@ +"""Audit theme variable consumers with deliberately distinct values in real NPM. + +The temporary palette is browser-only. Reload afterwards restores the injected +local theme. Screenshots complement the computed-style assertions. +""" +import json +import os +import re +from pathlib import Path +from playwright.sync_api import sync_playwright + +OUT = Path(__file__).resolve().parents[1] / 'artifacts/nginx-proxy-manager/707/variable-audit' +OUT.mkdir(parents=True, exist_ok=True) +PALETTE = { + '--main-bg-color': 'linear-gradient(30deg, #102030, #304050) center/cover fixed', + '--modal-bg-color': 'radial-gradient(ellipse at center, #304860, #182838) center/cover fixed', + '--modal-header-color': 'linear-gradient(90deg, #403060, #283848) center/cover fixed', + '--modal-footer-color': '#273941', '--drop-down-menu-bg': '#35475b', + '--button-color': '#466078', '--button-color-hover': '#6c7e90', + '--button-text': '#edf1f3', '--button-text-hover': '#ffedd0', + '--accent-color': '150, 120, 200', '--accent-color-hover': 'rgba(180, 140, 220, .9)', + '--link-color': '#abcdef', '--link-color-hover': '#fedcba', + '--label-text-color': '#142536', '--text': '#d9e2ec', + '--text-hover': '#f4f1de', '--text-muted': '#93a5b8', +} +records = [] +with sync_playwright() as p: + for engine in ('chromium', 'firefox'): + browser = getattr(p, engine).launch() + for mode in ('light', 'dark'): + page = browser.new_page(viewport={'width':1440, 'height':1000},color_scheme=mode) + page.goto('http://127.0.0.1:18084') + page.locator('input[name=email]').fill('admin@example.com') + page.locator('input[name=password]').fill(os.environ.get('NPM_DEV_PASSWORD','adminadmin')) + page.get_by_role('button',name='Sign in',exact=True).click() + page.get_by_text('0 Redirection Hosts',exact=True).wait_for() + prefix = f'{engine}-{mode}' + + def palette(): + page.add_style_tag(content=':root {'+';'.join(k+':'+v for k,v in PALETTE.items())+'}') + page.mouse.move(0,0);page.wait_for_timeout(650) + + def check(locator, prop, variable, label): + result = locator.evaluate('''(el,args)=>{ + const [prop,variable] = args; + const probe=document.createElement('i');probe.style.setProperty(prop.startsWith('background-')?'background':prop,`var(${variable})`);document.body.append(probe); + const expected=getComputedStyle(probe).getPropertyValue(prop), actual=getComputedStyle(el).getPropertyValue(prop);probe.remove();return {expected,actual}; + }''',[prop,variable]) + norm=lambda x:re.sub(r'rgba\((\d+, \d+, \d+), 1\)',r'rgb(\1)',x) + assert norm(result['actual'])==norm(result['expected']), (prefix,label,prop,variable,result) + records.append({'case':prefix,'consumer':label,'property':prop,'variable':variable,**result}) + + def hover(locator): + locator.hover();page.wait_for_timeout(650) + + def shot(name): + page.wait_for_timeout(300);page.screenshot(path=str(OUT/f'{prefix}-{name}.png'),full_page=True) + + palette() + check(page.locator('body'),'background-image','--main-bg-color','page') + for name,loc in [('navigation',page.get_by_role('link',name='Access Lists',exact=True)),('footer',page.get_by_role('link',name='Fork me on Github',exact=True)),('dashboard',page.get_by_role('link',name='2 Proxy Hosts',exact=True))]: + page.mouse.move(0,0);page.wait_for_timeout(350) + check(loc,'color','--link-color',name) + hover(loc);check(loc,'color','--link-color-hover',name+' hover');shot(name+'-hover') + page.mouse.move(0,0);page.keyboard.press('Tab');loc.focus();page.wait_for_timeout(350) + check(loc,'color','--link-color-hover',name+' focus') + page.locator('body').click(position={'x':1,'y':200}) + page.goto('http://127.0.0.1:18084/nginx/proxy');page.get_by_role('button',name='Add Proxy Host',exact=True).wait_for();palette() + add=page.get_by_role('button',name='Add Proxy Host',exact=True) + check(add,'background-color','--button-color','primary button');check(add,'color','--button-text','primary button') + hover(add);check(add,'background-color','--button-color-hover','primary hover');check(add,'color','--button-text-hover','primary hover') + add.click();page.locator('.modal-content').wait_for();page.wait_for_timeout(400) + for sel,var in [('.modal-content','--modal-bg-color'),('.modal-header','--modal-header-color'),('.modal-footer','--modal-footer-color')]: + check(page.locator(sel),'background-image',var,sel);check(page.locator(sel),'background-color',var,sel) + check(page.locator('.modal .form-label').first,'color','--text','form label') + check(page.locator('.modal h4'),'color','--text-hover','heading') + check(page.locator('.react-select__placeholder').first,'color','--text-muted','select placeholder') + domain=page.locator('#domainNames input[role=combobox]');domain.fill('variables.example.test');page.keyboard.press('Enter');page.mouse.move(0,0) + # accent-color is an RGB triplet, so its consumer needs rgb(). + chip=page.locator('.react-select__multi-value') + assert chip.evaluate('(el)=>getComputedStyle(el).backgroundColor')=='rgb(150, 120, 200)' + records.append({'case':prefix,'consumer':'domain chip background','variable':'--accent-color','actual':'rgb(150, 120, 200)'}) + check(page.locator('.react-select__multi-value__label'),'color','--label-text-color','domain label') + remove=page.locator('.react-select__multi-value__remove');hover(remove);check(remove,'background-color','--accent-color-hover','domain remove hover') + shot('labels') + field=page.locator('#forwardHost');field.focus();page.wait_for_timeout(400);check(field,'color','--text-hover','focused input') + page.locator('.react-select__control').nth(1).click();page.wait_for_timeout(400) + check(page.locator('.react-select__menu'),'background-color','--drop-down-menu-bg','select menu');shot('menu') + page.keyboard.press('Escape');page.get_by_role('tab',name='SSL',exact=True).click();shot('radial-body') + page.locator('.modal a[title=Settings]').click() + page.locator('.w-tc-editor textarea').fill('# preview\nproxy_set_header X-Theme test;') + check(page.locator('.w-tc-editor code'),'color','--text','editor plain text') + check(page.locator('.w-tc-editor .token.comment'),'color','--text-muted','editor comment') + check(page.locator('.w-tc-editor .token.keyword'),'color','--text-hover','editor keyword') + assert page.locator('.w-tc-editor textarea').evaluate('(el)=>getComputedStyle(el).webkitTextFillColor')=='rgba(0, 0, 0, 0)' + shot('editor') + page.locator('.modal .btn-close').click();page.locator('.modal').wait_for(state='hidden') + # Reload removes the diagnostic palette and restores real injection. + page.reload();page.get_by_role('button',name='Add Proxy Host',exact=True).wait_for() + page.close() + browser.close() +(OUT/'results.json').write_text(json.dumps(records,indent=2)+'\n') +print(f'{len(records)} variable consumer checks passed')