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/.github/workflows/minify-and-deploy.yml b/.github/workflows/minify-and-deploy.yml index 2636902b9f..85a5ba8fc8 100644 --- a/.github/workflows/minify-and-deploy.yml +++ b/.github/workflows/minify-and-deploy.yml @@ -72,7 +72,7 @@ jobs: publish_dir: ./ publish_branch: live github_token: ${{ secrets.GITHUB_TOKEN }} - exclude_assets: '' + exclude_assets: 'dev,AGENTS.md' - name: Deploy Develop uses: peaceiris/actions-gh-pages@v3.9.3 if: ${{ github.ref == 'refs/heads/develop' || github.event.inputs.branch == 'develop' }} @@ -80,7 +80,7 @@ jobs: publish_dir: ./ publish_branch: live_develop github_token: ${{ secrets.GITHUB_TOKEN }} - exclude_assets: '' + exclude_assets: 'dev,AGENTS.md' - name: Deploy Testing uses: peaceiris/actions-gh-pages@v3.9.3 if: ${{ github.ref == 'refs/heads/testing' || github.event.inputs.branch == 'testing' }} @@ -88,7 +88,7 @@ jobs: publish_dir: ./ publish_branch: live_testing github_token: ${{ secrets.GITHUB_TOKEN }} - exclude_assets: '' + exclude_assets: 'dev,AGENTS.md' - name: Clear CF Cache run: | curl -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..dcddbc08e9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,77 @@ +# Working on theme.park + +This project injects CSS into other applications. Reproduce CSS bugs in the +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. +- Preserve existing work. Inspect the working tree before editing or changing + branches; do not reset unrelated changes. +- Always create task branches from freshly fetched `origin/develop`, including + CSS fixes and development tooling. Verify the starting commit before editing. + The normal flow is a task branch from `develop`, then a merge back into + `develop`. Target pull requests at `develop` as well. +- Reserve `testing` for potentially breaking changes that need isolation from + users of `develop`. It is not a required step for ordinary fixes. Users of + `testing` accept disruption; protecting them is not a release constraint. +- Add development tools when an app needs them. Record setup steps in + `dev//README.md` so the next session can reuse them. +- Document discoveries that would help another session before finishing a task. + Put shared workflow lessons in `dev/README.md` and app-specific lessons in + `dev//README.md`. Apply the `unslop` skill to these notes. Explain what + happened, why it matters, and how to reproduce or avoid it. Update existing + guidance instead of duplicating it, and distinguish verified findings from + 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 + 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). + +## Read before starting + +1. Read [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md). +2. Read [dev/README.md](dev/README.md) and any `dev//README.md`. +3. Consult the [setup docs](https://docs.theme-park.dev/setup/), the target app's + documentation page, and its entry in + [mkdocs.yml](https://github.com/themepark-dev/tp-docs/blob/main/mkdocs.yml). + The documentation source is the separate + [tp-docs repository](https://github.com/themepark-dev/tp-docs). + Check app-specific exceptions before using a generic installation recipe. +4. Read the issue, relevant base CSS, its imports, and recent fixes. Record the + reported app version and installation method. Check whether an existing + commit already addresses the issue. + +## Implementation and verification + +- Follow `dev/README.md`. Reuse the app's setup or add one with sample data and + startup commands. +- Edit `css/base//-base.css` for app-specific styling. Inspect shared + files in `css/defaults` before changing them. Servarr apps share styles. +- Use existing variables in `css/theme-options` and + `css/community-theme-options`. Preserve their value formats and the meaning + of status colors. Scope overrides to the affected component where possible. +- Generated `/.css` wrappers come from `themes.py`. Do not hand-edit + generated output or populate the working tree with build artifacts. +- Confirm the browser loads the edited local stylesheet and its imports. + Verify the final files through the documented injection method, even if you + used temporary browser styles while investigating. +- Save before/after screenshots and inspect the rendered result. Computed-style + assertions alone do not establish visibility, contrast, or correct layout. +- Test the affected interaction states, theme options, browsers, and viewport + sizes. Include solid-color, radial-gradient, and linear-gradient backgrounds. + Check the actual variables used by the component, not just the page background. + Follow the screenshot requirements in + CONTRIBUTING.md when adding a new application theme. +- Report app and browser versions, installation method, evidence, and untested + areas. Do not claim an issue fixed from a screenshot or source inspection alone. +- Add any setup requirements you discover to the app's development notes. + Record other UI defects found during inspection separately from the reported + bug. Explain which ones the patch fixes and which remain. + Target `develop` for contributions. Push only when the maintainer instructs you. diff --git a/css/addons/bazarr/bazarr-4k-logo/bazarr-4k-logo.css b/css/addons/bazarr/bazarr-4k-logo/bazarr-4k-logo.css index 18eb5bc1e2..6a3a45da57 100644 --- a/css/addons/bazarr/bazarr-4k-logo/bazarr-4k-logo.css +++ b/css/addons/bazarr/bazarr-4k-logo/bazarr-4k-logo.css @@ -4,4 +4,12 @@ #root>div>header>div>div.bazarr-Group-root[class*="bazarr-"]>div>span:after { content: " 4K"; - } \ No newline at end of file + } +/* Current Mantine frontend. */ +.mantine-AppShell-header .mantine-Avatar-image { + content: url("/css/addons/bazarr/bazarr-4k-logo/bazarr4k.png"); +} + +.mantine-AppShell-header .mantine-Badge-root[data-variant="brand"] .mantine-Badge-label::after { + content: " 4K"; +} diff --git a/css/addons/bazarr/bazarr-darker/bazarr-darker.css b/css/addons/bazarr/bazarr-darker/bazarr-darker.css index 28033f0493..62b5c17ece 100644 --- a/css/addons/bazarr/bazarr-darker/bazarr-darker.css +++ b/css/addons/bazarr/bazarr-darker/bazarr-darker.css @@ -52,4 +52,16 @@ #root>div>div>main>div>div>div.bazarr-Group-root[class*="bazarr-"], #root>div>div>main>form>div.bazarr-Group-root[class*="bazarr-"] { background: #262626 !important; -} \ No newline at end of file +} +/* Current Mantine frontend. */ +.mantine-AppShell-header { + background: var(--header-color); +} + +.mantine-AppShell-navbar { + background: var(--side-menu-color); +} + +.mantine-AppShell-main .mantine-Group-root[class*="_group_"] { + background: var(--toolbar-background); +} diff --git a/css/addons/whisparr/whisparr-4k-logo/whisparr-4k-logo.css b/css/addons/whisparr/whisparr-4k-logo/whisparr-4k-logo.css new file mode 100644 index 0000000000..fe1105718a --- /dev/null +++ b/css/addons/whisparr/whisparr-4k-logo/whisparr-4k-logo.css @@ -0,0 +1,22 @@ +/* Whisparr v2 and the compact Whisparr v3 header. */ +img[class*="PageHeader-logo-"], +img[class*="PageSidebar-logo-"], +.panel-header > img.logo { + content: url("whisparr-4k.svg"); + width: 32px; + height: 32px; +} + +/* Whisparr v3 desktop header. */ +img[class*="PageHeader-logoFull-"] { + content: url("whisparr-4k.svg"); + width: 40px; + height: 40px; +} + +img[class*="LoadingPage-logoFull-"] { + content: url("whisparr-4k.svg"); + width: 48px; + height: 48px; + opacity: 1; +} diff --git a/css/addons/whisparr/whisparr-4k-logo/whisparr-4k.svg b/css/addons/whisparr/whisparr-4k-logo/whisparr-4k.svg new file mode 100644 index 0000000000..adb52148b1 --- /dev/null +++ b/css/addons/whisparr/whisparr-4k-logo/whisparr-4k.svg @@ -0,0 +1,11 @@ + + Whisparr 4K + + + + + + + + + diff --git a/css/base/bazarr/bazarr-base.css b/css/base/bazarr/bazarr-base.css index f76210a5e5..cd5ed2f97a 100644 --- a/css/base/bazarr/bazarr-base.css +++ b/css/base/bazarr/bazarr-base.css @@ -564,4 +564,392 @@ code, .bazarr-LoadingOverlay-root[class*="bazarr-"] svg { stroke: rgb(var(--accent-color)); -} \ No newline at end of file +} +/* Bazarr 1.4.4+ uses Mantine's public component classes. Keep the legacy + bazarr-* rules above for installations on the older frontend. */ +html[data-mantine-color-scheme] { + /* Mantine also defines --button-color on each button. Resolve the theme + value here before that local variable can shadow it. */ + --bazarr-button-background: var(--button-color); + --mantine-color-text: var(--text); + --mantine-color-dimmed: var(--text-muted); + --mantine-color-anchor: var(--link-color); + --mantine-color-placeholder: var(--text-muted); + --mantine-color-default: var(--transparency-dark-25); + --mantine-color-default-hover: var(--transparency-light-10); + --mantine-color-default-color: var(--text); + --mantine-color-default-border: var(--transparency-light-15); + --mantine-color-disabled: var(--transparency-light-10); + --mantine-color-disabled-color: var(--text-muted); + --mantine-color-disabled-border: var(--transparency-light-10); + --mantine-primary-color-filled: rgb(var(--accent-color)); + --mantine-primary-color-filled-hover: var(--accent-color-hover); + --mantine-primary-color-light: rgba(var(--accent-color), .2); + --mantine-primary-color-light-hover: rgba(var(--accent-color), .3); + --mantine-primary-color-light-color: var(--text-hover); + --mantine-color-brand-filled: rgb(var(--accent-color)); + --mantine-color-brand-filled-hover: var(--accent-color-hover); + --mantine-color-brand-light: rgba(var(--accent-color), .2); + --mantine-color-brand-light-hover: rgba(var(--accent-color), .3); + --mantine-color-brand-light-color: var(--text-hover); + --mantine-color-brand-text: var(--link-color); +} + +/* Background variables may contain complete gradient background shorthands. */ +.mantine-AppShell-header, +.mantine-AppShell-navbar { + background: var(--main-bg-color); + border-color: var(--transparency-light-15); + color: var(--text); +} + +.mantine-AppShell-main { + background: transparent; +} + +.mantine-AppShell-navbar a[href] { + color: var(--text); + border-left: 2px solid transparent; +} + +.mantine-AppShell-navbar a[href] > .mantine-Text-root { + color: inherit; +} + +html[data-mantine-color-scheme] .mantine-AppShell-navbar a[href]:hover { + background: var(--transparency-light-10); + color: var(--text-hover); +} + +html[data-mantine-color-scheme] .mantine-AppShell-navbar a[aria-current="page"] { + background: var(--transparency-dark-25); + border-left-color: rgb(var(--accent-color)); + color: var(--text-hover); +} + +/* Toolbox has no public class; scope its module name to the main area. */ +.mantine-AppShell-main .mantine-Group-root[class*="_group_"] { + background: var(--transparency-dark-25); +} + +.mantine-Divider-root { + --divider-color: var(--transparency-light-15); +} + +.mantine-Input-input { + background: var(--transparency-dark-25); + color: var(--text); + border-color: var(--transparency-light-15); +} + +.mantine-Input-input:focus, +.mantine-Input-input:focus-within { + border-color: rgb(var(--accent-color)); +} + +.mantine-Input-input[data-error] { + border-color: var(--mantine-color-error); + color: var(--mantine-color-error); +} + +.mantine-Input-input:disabled, +.mantine-Input-input[data-disabled] { + background: var(--transparency-light-10); + color: var(--text-muted); +} + +.mantine-Input-section, +.mantine-ComboboxChevron-chevron { + color: var(--text-muted); +} + +.mantine-NumberInput-control { + color: var(--text); + border-color: var(--transparency-light-15); +} + +.mantine-NumberInput-control:hover, +.mantine-CloseButton-root:hover { + background: var(--transparency-light-15); + color: var(--text-hover); +} + +.mantine-Button-root:not(:disabled):not([data-disabled]):not([data-variant="danger"]):not([style*="--mantine-color-red-"]) { + background: var(--bazarr-button-background); + color: var(--button-text); + border-color: transparent; +} + +.mantine-Button-root:not([data-variant="danger"]):not([style*="--mantine-color-red-"]):hover:not(:disabled):not([data-disabled]) { + background: var(--button-color-hover); + color: var(--button-text-hover); +} + +.mantine-Button-root:disabled, +.mantine-Button-root[data-disabled] { + background: var(--transparency-light-10); + color: var(--text-muted); + border-color: transparent; +} + +.mantine-Button-label, +.mantine-Button-label .mantine-Text-root { + color: inherit !important; +} + +/* Action icons can carry red/yellow status colors; retain those inline colors. */ +.mantine-ActionIcon-root { + background: transparent; + color: var(--text); +} + +.mantine-ActionIcon-root:hover:not(:disabled):not([data-disabled]) { + background: var(--transparency-light-15); +} + +.mantine-ActionIcon-root:disabled, +.mantine-ActionIcon-root[data-disabled] { + color: var(--text-muted); + opacity: .5; +} + +.mantine-Pagination-control { + background: var(--transparency-dark-25); + color: var(--text); + border-color: var(--transparency-light-15); +} + +.mantine-Pagination-control:hover:not(:disabled):not([data-disabled]), +.mantine-Pagination-control[data-active] { + background: var(--bazarr-button-background); + color: var(--button-text); +} + +.mantine-Pagination-control:disabled, +.mantine-Pagination-control[data-disabled] { + color: var(--text-muted); +} + +.mantine-Checkbox-input, +.mantine-Radio-radio, +.mantine-Switch-track { + background: var(--transparency-dark-25); + border-color: var(--transparency-light-25); +} + +.mantine-Checkbox-input:checked, +.mantine-Radio-radio:checked, +.mantine-Switch-input:checked + .mantine-Switch-track { + background: rgb(var(--accent-color)); + border-color: rgb(var(--accent-color)); +} + +.mantine-Checkbox-icon, +.mantine-Radio-icon { + color: var(--label-text-color); +} + +.mantine-Switch-thumb { + background: var(--text-hover); + border-color: transparent; +} + +.mantine-Checkbox-input:disabled, +.mantine-Radio-radio:disabled, +.mantine-Switch-input:disabled + .mantine-Switch-track { + opacity: .5; +} + +.mantine-Popover-dropdown, +.mantine-Menu-dropdown, +.mantine-Tooltip-tooltip { + background: var(--drop-down-menu-bg); + border-color: var(--transparency-light-15); + color: var(--text); +} + +.mantine-Menu-item { + color: var(--menu-item-color, var(--text)); +} + +.mantine-Combobox-option[data-combobox-selected], +.mantine-Select-option[data-combobox-selected], +.mantine-MultiSelect-option[data-combobox-selected], +.mantine-Autocomplete-option[data-combobox-selected] { + background: var(--transparency-light-15); + color: var(--text-hover); +} + +.mantine-Menu-divider { + border-color: var(--transparency-light-15); +} + +.mantine-Modal-content, +.mantine-Drawer-content { + background: var(--modal-bg-color); + color: var(--text); +} + +.mantine-Modal-header, +.mantine-Drawer-header { + background: var(--modal-header-color); + color: var(--text-hover); + border-bottom: 1px solid var(--transparency-light-10); +} + +.mantine-Modal-title, +.mantine-Drawer-title, +.mantine-CloseButton-root { + color: var(--text-hover); +} + +.mantine-Table-table { + --table-border-color: var(--transparency-light-10); + --table-striped-color: var(--transparency-dark-10); + --table-highlight-on-hover-color: var(--transparency-light-10); + color: var(--text); +} + +.mantine-Table-th { + color: var(--text-hover); +} + +.mantine-Paper-root:not(.mantine-Modal-content):not(.mantine-Drawer-content):not(.mantine-Popover-dropdown):not(.mantine-Menu-dropdown) { + background: var(--transparency-dark-25); + color: var(--text); + border-color: var(--transparency-light-15); +} + +.mantine-Card-root, +.mantine-AppShell-main [class*="_card_"] { + background: var(--transparency-dark-25); + border-color: var(--transparency-light-15); +} + +.mantine-AppShell-main [class*="_card_"]:hover { + border-color: rgb(var(--accent-color)); +} + +/* Keep warning, highlight and disabled subtitle badges distinguishable. */ +.mantine-Badge-root:not([data-variant]), +.mantine-Badge-root[data-variant="brand"] { + background: rgba(var(--accent-color), .25); + color: var(--text-hover); +} + +.mantine-Badge-label { + color: inherit !important; +} + +.mantine-Pill-root, +.mantine-Code-root { + background: var(--transparency-light-10); + color: var(--text); +} + +.mantine-Notification-root { + background: var(--drop-down-menu-bg); + border-color: var(--transparency-light-15); +} + +.mantine-Notification-title { + color: var(--text-hover); +} + +.mantine-Notification-description { + color: var(--text); +} + +.mantine-ScrollArea-thumb { + background: var(--transparency-light-25); +} + +/* These dialogs place their action row at the end of the body stack. */ +.mantine-Modal-body > .mantine-Stack-root > .mantine-Group-root:last-child, +.mantine-Modal-body > form > .mantine-Stack-root > .mantine-Group-root:last-child, +.mantine-Modal-body > .mantine-Group-root:last-child { + background: var(--modal-footer-color); + padding: var(--mantine-spacing-md); + margin: 0 calc(-1 * var(--mantine-spacing-md)) calc(-1 * var(--mantine-spacing-md)); +} + +.mantine-Accordion-item { + border-color: var(--transparency-light-15); +} + +.mantine-Accordion-control { + color: var(--text); +} + +.mantine-Accordion-control:hover { + background: var(--transparency-light-10); +} + +.mantine-Progress-root { + background: var(--transparency-light-15); +} + +.mantine-Table-table a { + color: var(--text); +} + +.mantine-Table-table a:hover { + color: var(--text-hover); + text-decoration: underline; +} + +.mantine-Tabs-list::before { + border-color: var(--transparency-light-15); +} + +.mantine-Tabs-tab { + color: var(--text); +} + +.mantine-Tabs-tab:hover { + background: var(--transparency-light-10); +} + +.mantine-Tabs-tab[data-active] { + border-color: rgb(var(--accent-color)); + color: var(--text-hover); +} + +.mantine-Slider-track::before { + background: var(--transparency-light-15); +} + +.mantine-Slider-bar { + background: rgb(var(--accent-color)); +} + +.mantine-Slider-thumb { + border-color: rgb(var(--accent-color)); +} + +.mantine-Dropzone-root { + background: var(--transparency-dark-25); + color: var(--text); + border-color: var(--transparency-light-25); +} + +.mantine-Dropzone-root:hover { + background: var(--transparency-light-10); +} + +/* Mantine's display rules otherwise expose controls Bazarr marks hidden. */ +.mantine-UnstyledButton-root[hidden], +.mantine-Badge-root[hidden] { + display: none; +} + +.mantine-Menu-item:hover:not([data-disabled]) { + background: var(--transparency-light-15); + color: var(--menu-item-color, var(--text-hover)); +} + +/* Movie language badges use explicit Mantine colors rather than variants. */ +.mantine-Badge-root[style*="--badge-bg: var(--mantine-color-"]:not([style*="--mantine-color-brand-"]) { + background: var(--badge-bg); + color: var(--badge-color); +} diff --git a/css/base/dozzle/dozzle-base.css b/css/base/dozzle/dozzle-base.css index 50ab3ff7e9..ada8646f7b 100644 --- a/css/base/dozzle/dozzle-base.css +++ b/css/base/dozzle/dozzle-base.css @@ -13,268 +13,249 @@ @import url("/css/defaults/placeholders.css"); @import url("/css/defaults/transparent.css"); -:root { - --scheme-main-ter: var(--main-bg-color); - --text-strong-color: var(--button-text-hover); - --border-color: var(--transparency-light-25); - --logo-color: rgb(var(--accent-color)); - --body-background-color: var(--main-bg-color); - --border-hover-color: rgb(var(--accent-color)); -} - -* { - outline: none; -} - +/* Dozzle 11. DaisyUI variables are colors, while theme.park background + variables can include gradients, positioning and attachment. */ html, body { - background: var(--main-bg-color) !important; - 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: var(--main-bg-color); color: var(--text); } -h1, -h2, -h3, -h4, -h5, -h6, -section header, -.menu-label { - color: var(--text-hover) !important; +html[data-theme] { + --color-base-100: transparent; + --color-base-200: var(--transparency-dark-15); + --color-base-300: var(--transparency-dark-25); + --color-base-content: var(--text); + --color-primary: rgb(var(--accent-color)); + --color-primary-content: var(--label-text-color); + --color-secondary: var(--button-color); + --color-secondary-content: var(--button-text); + --color-neutral: var(--transparency-dark-25); + --color-neutral-content: var(--text); + /* Mix ANSI hues with theme text so native light mode cannot leave dark + terminal colors on a dark theme. Keep each severity's original hue. */ + --ansi-black: var(--text-muted); + --ansi-red: color-mix(in srgb, var(--text) 55%, #ff0000); + --ansi-green: color-mix(in srgb, var(--text) 55%, #00a000); + --ansi-yellow: color-mix(in srgb, var(--text) 55%, #c09000); + --ansi-blue: color-mix(in srgb, var(--text) 55%, #0066ff); + --ansi-magenta: color-mix(in srgb, var(--text) 55%, #cc00cc); + --ansi-cyan: color-mix(in srgb, var(--text) 55%, #009999); + --ansi-white: var(--text); + --ansi-bright-black: var(--text-muted); + --ansi-bright-red: var(--ansi-red); + --ansi-bright-green: var(--ansi-green); + --ansi-bright-yellow: var(--ansi-yellow); + --ansi-bright-blue: var(--ansi-blue); + --ansi-bright-magenta: var(--ansi-magenta); + --ansi-bright-cyan: var(--ansi-cyan); + --ansi-bright-white: var(--text-hover); +} + +/* Keep focus visible on custom controls as well as form fields. */ +html[data-theme] :focus-visible { + outline: 2px solid rgb(var(--accent-color)) !important; + outline-offset: 2px; +} + +html[data-theme] .nav-group-toggle { + color: var(--text); } +html[data-theme] .nav-group-toggle:hover { + color: var(--text-hover); +} -/* Scrollbar */ - -html.has-custom-scrollbars ::-webkit-scrollbar-thumb { - background: var(--transparency-light-25); - outline: 1px solid #0000; - border-radius: 4px; +html[data-theme] .nav-item { + color: var(--text); } -html.has-custom-scrollbars ::-webkit-scrollbar-thumb:active, -html.has-custom-scrollbars ::-webkit-scrollbar-thumb:hover { - background: var(--transparency-light-45); +html[data-theme] .nav-item:hover { + color: var(--text-hover); + background: var(--transparency-light-10); +} +html[data-theme] .nav-item.is-active, +html[data-theme] .nav-item.is-merged { + color: var(--text-hover); + background: rgba(var(--accent-color), .15); + box-shadow: inset 2px 0 rgb(var(--accent-color)); } -html.has-custom-scrollbars ::-webkit-scrollbar-track { - background: #1f1f1f; +html[data-theme] .nav-item.is-muted { + color: var(--text-muted); } -html.has-custom-scrollbars ::-webkit-scrollbar-track:hover { - background: #1f1f1f; +html[data-theme] .btn:not(.btn-error, .btn-warning, .btn-success, .btn-info, .btn-ghost, .btn-link) { + background: var(--button-color); + color: var(--button-text); + border-color: transparent; } -html.has-custom-scrollbars section main { - scrollbar-color: #353535 transparent; - scrollbar-width: thin +html[data-theme] .btn:is(.btn-ghost, .btn-link) { + background: transparent; + color: var(--text); } -/* Text important */ -p, -.menu-list a { - color: var(--text) !important; +html[data-theme] .btn:not(.btn-error, .btn-warning, .btn-success, .btn-info):not(:disabled, .btn-disabled, [aria-disabled="true"]):hover { + background: var(--button-color-hover); + color: var(--button-text-hover); +} +html[data-theme] .btn:is(.btn-error, .btn-warning, .btn-success, .btn-info):not(:disabled, .btn-disabled, [aria-disabled="true"]):hover { + background: var(--btn-color); + color: var(--btn-fg); + filter: brightness(1.1); } -.panel-heading { - background-color: rgb(var(--accent-color)); - color: var(--label-text-color) !important; +html[data-theme] .btn:is(:disabled, .btn-disabled, [aria-disabled="true"]) { + background: var(--transparency-dark-15); + color: var(--text-muted); + border-color: transparent; } -.panel-block { +html[data-theme] :is(.input, .select, .textarea) { + background: var(--transparency-dark-25); color: var(--text); + border-color: color-mix(in srgb, var(--text) 25%, transparent); } -.panel-tabs a.is-active { - border-bottom-color: rgb(var(--accent-color)); - color: rgb(var(--accent-color)); +html[data-theme] :is(.input, .select, .textarea):focus-within { + border-color: rgb(var(--accent-color)); } -.panel-tabs a { - border-bottom: 1px solid var(--border-color); - color: var(--text); +html[data-theme] :is(.input-error, .input-error\!, .select-error, .textarea-error), +html[data-theme] :is(.input-error, .input-error\!, .select-error, .textarea-error):focus-within { + border-color: var(--color-error); } -.panel-tabs a:hover { - border-bottom: 1px solid var(--border-color); - color: var(--text-strong-color); +html[data-theme] .cm-editor { + --color-success: var(--ansi-green); + --color-info: var(--ansi-blue); + --color-warning: var(--ansi-yellow); + --color-secondary: var(--ansi-magenta); } -a.panel-block:hover, -label.panel-block:hover { - background-color: var(--transparency-light-10); - color: var(--text-strong-color); +html[data-theme] :is(.cm-placeholder, .cm-gutters) { + color: var(--text-muted); } -/* Side Menu*/ -.menu-list a:hover { - background-color: var(--transparency-light-10); - color: var(--text-hover) !important; +html[data-theme] select option { + background: var(--drop-down-menu-bg); + color: var(--text); } -.menu-list a.is-active, -.menu-list a.is-active:hover { - background-color: rgb(var(--accent-color)); - color: var(--label-text-color) !important; +html[data-theme] :is(.popover-panel, .nav-menu-panel, .cm-tooltip) { + background: var(--drop-down-menu-bg) !important; + color: var(--text); } -.menu-list li:hover .column-icon:hover { - color: black !important; +html[data-theme] .toast > div, +html[data-theme] [data-testid="scrollable-header"] + .pointer-events-none > div { + background: var(--drop-down-menu-bg); } -li.exited a { - color: var(--text-muted) !important; +html[data-theme] .nav-menu-item:hover { + background: var(--transparency-light-10); } -.select select, -.textarea, -.input, -.dropdown-content { +html[data-theme] .modal-box, +html[data-theme] .modal-box.bg-transparent > div { 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; - border-color: rgba(255, 255, 255, .1); - border-radius: 4px; - color: var(--text-strong-color); -} - -.autocomplete .dropdown-item.is-hovered, -a.dropdown-item:hover, -button.dropdown-item:hover { - background: var(--transparency-light-25); - color: var(--text-strong-color); -} - -.column-icon[data-v-35775614]:hover { - color: rgb(var(--accent-color)); -} - -.select select:focus, -.textarea:focus, -.input:focus, -.select select.is-focused, -.is-focused.textarea, -.is-focused.input, -.select select:active, -.textarea:active, -.input:active, -.select select.is-active, -.is-active.textarea, -.is-active.input { - border-color: rgb(var(--accent-color)); - box-shadow: 0 0 0 0.125em rgba(var(--accent-color), .25); + color: var(--text); } -/* Settings buttons */ -.button { - background: var(--button-color); - color: var(--button-text); - border-color: var(--button-color); +html[data-theme] .modal-box.bg-transparent { + background: transparent; } -section .is-scrollbar-notification button { - background: var(--button-color) !important; - color: var(--button-text) !important; - border-color: var(--button-color) !important; +html[data-theme] .modal-right .modal-box header { + /* The inset drawer heading shares the body's background. Painting a + separate gradient here creates a rectangle inside the modal padding. */ + background: transparent; } -.b-radio.radio.button.is-selected { - background-color: var(--button-color-hover); - border-color: transparent; - color: var(--text-strong-color); +html[data-theme] .modal-right .modal-box .sticky.bottom-0 { + background: var(--modal-footer-color); } -.button:hover { - background: var(--button-color-hover) !important; - border-color: var(--button-color-hover) !important; - color: var(--button-text-hover) !important; +/* The search dialog wraps its content in a transparent, padded modal-box. */ +html[data-theme] .modal-box.bg-transparent > div > :first-child { + background: var(--modal-header-color); } -.button:active, -.button.is-active { - background: var(--button-color-hover); - border-color: var(--button-color-hover); - color: var(--text-strong-color); +html[data-theme] .modal-box.bg-transparent > div > :last-child { + background: var(--modal-footer-color); } -.is-settings-control { - background: var(--button-color); +/* Fixed navigation and sticky toolbars must cover logs scrolling behind them. */ +html[data-theme] nav[data-testid="navigation"], +html[data-theme] [data-testid="scrollable-header"] { + background: var(--main-bg-color); +} + +html[data-theme] .splitpanes__splitter { + background: var(--transparency-dark-25); +} + +html[data-theme] mark { color: var(--button-text); - border-color: transparent; } -.is-settings-control:hover { - border-color: var(--button-color-hover) !important; - background: var(--button-color-hover) !important; - color: var(--button-text-hover) !important; +html[data-theme] .json-boolean { + color: var(--ansi-magenta); } -#hide-nav { - background: var(--button-color) !important; - color: var(--button-text) !important; +html[data-theme] .json-key { + color: var(--ansi-blue); } -#hide-nav:hover { - border-color: var(--button-color-hover) !important; - background: var(--button-color-hover) !important; - color: var(--button-text-hover) !important; +html[data-theme] .json-string { + color: var(--ansi-green); } -code { - background: var(--transparency-dark-35) !important; +html[data-theme] .json-number { + color: var(--ansi-yellow); } -.switch input[type=checkbox]:checked+.check { - background: var(--button-color); +html[data-theme] .json-null { + color: var(--ansi-red); } -.switch:hover input[type=checkbox]:checked+.check { - background: var(--button-color-hover); +html[data-theme] :is(.text-primary, .text-secondary) { + color: var(--text); } -.switch input[type=checkbox]:focus:checked+.check, -.switch input[type=checkbox]:active:checked+.check { - box-shadow: 0 0 0.5em rgb(var(--accent-color), .8); +html[data-theme] :is(.text-base-content\/40, .text-base-content\/45, .text-base-content\/50, .text-base-content\/55, .text-base-content\/60) { + color: var(--text-muted); } -/* Events */ +html[data-theme] code { + color: inherit; + background: var(--transparency-dark-15) !important; +} -.events { - background: var(--transparency-dark-35); +html[data-theme] .modal-right .field-row code { + background: transparent !important; } -.scroll-progress svg circle { - fill: rgba(255, 255, 255, .45) !important; - stroke: rgb(var(--accent-color)) !important; +/* data-logs identifies the list even when error highlighting is disabled. */ +html[data-theme] [data-logs] { + background: var(--transparency-dark-35); } -.scroll-progress span { - color: var(--text-strong-color) !important; +html[data-theme] :is(.link, .link-primary, a[rel~="external"]) { + color: var(--link-color); } -.splitpanes--vertical>.splitpanes__splitter:hover { - background: rgb(var(--accent-color)) !important; +html[data-theme] :is(.link, .link-primary, a[rel~="external"]):hover { + color: var(--link-color-hover); } -.events.medium { - background: var(--transparency-dark-45); +html[data-theme].has-custom-scrollbars * { + scrollbar-color: var(--transparency-light-25) transparent; } -.date { - color: rgb(var(--accent-color)) !important; - background: var(--transparency-dark-25) !important; -} \ No newline at end of file +html[data-theme].has-custom-scrollbars ::-webkit-scrollbar-track { + background: transparent; +} diff --git a/css/base/jellyfin/jellyfin-base.css b/css/base/jellyfin/jellyfin-base.css index 079c4c2bc5..198714e1a9 100644 --- a/css/base/jellyfin/jellyfin-base.css +++ b/css/base/jellyfin/jellyfin-base.css @@ -60,13 +60,13 @@ h6, color: var(--text-hover); } -a:not(.emby-button), +a:not(.emby-button):not(.MuiButtonBase-root), .cardText.cardTextCentered.cardText-first>button, .emby-linkbutton>a { color: var(--link-color) !important; } -a:hover:not(.emby-button), +a:hover:not(.emby-button):not(.MuiButtonBase-root), .cardText.cardTextCentered.cardText-first>button:hover, .emby-linkbutton>a:hover { color: var(--link-color-hover) !important; @@ -936,4 +936,157 @@ html { .layout-tv .emby-button.detailFloatingButton:focus { background-color: #f2f2f2; color: rgb(var(--theme-accent-text-color)); -} \ No newline at end of file +} +/* Jellyfin 12 uses the jf palette for both Material UI and legacy components. */ +:root[data-theme] { + --jf-palette-primary-main: rgb(var(--accent-color)); + --jf-palette-primary-light: var(--link-color-hover); + --jf-palette-primary-dark: var(--button-color); + --jf-palette-primary-contrastText: var(--button-text); + --jf-palette-secondary-main: rgb(var(--accent-color)); + --jf-palette-secondary-light: var(--link-color-hover); + --jf-palette-secondary-dark: var(--button-color); + --jf-palette-secondary-contrastText: var(--button-text); + /* MUI inserts these channels into rgba(channel / opacity). */ + --jf-palette-primary-mainChannel: from rgb(var(--accent-color)) r g b; + --jf-palette-secondary-mainChannel: from rgb(var(--accent-color)) r g b; + --jf-palette-text-primary: var(--text); + --jf-palette-text-secondary: var(--text-muted); + --jf-palette-text-disabled: var(--text-muted); + --jf-palette-text-primaryChannel: from var(--text) r g b; + --jf-palette-text-secondaryChannel: from var(--text-muted) r g b; + --jf-palette-background-default: transparent; + --jf-palette-background-defaultImage: none; + --jf-palette-background-paper: var(--drop-down-menu-bg); + --jf-palette-surface-overlay: var(--transparency-dark-25); + --jf-palette-divider: var(--transparency-light-15); + --jf-palette-action-active: var(--text); + --jf-palette-action-hover: var(--transparency-light-10); + --jf-palette-action-selected: rgba(var(--accent-color), .2); + --jf-palette-action-focus: rgba(var(--accent-color), .2); + --jf-palette-action-disabled: var(--text-muted); + --jf-palette-action-disabledBackground: var(--transparency-dark-15); + --jf-palette-action-activeChannel: from var(--text) r g b; + --jf-palette-AppBar-defaultBg: var(--drop-down-menu-bg); + --jf-palette-AppBar-darkBg: var(--drop-down-menu-bg); + --jf-palette-AppBar-darkColor: var(--text); + --jf-palette-FilledInput-bg: var(--transparency-dark-25); + --jf-palette-FilledInput-hoverBg: var(--transparency-dark-35); + --jf-palette-FilledInput-disabledBg: var(--transparency-dark-15); + --jf-palette-TableCell-border: var(--transparency-light-15); + --jf-palette-Tooltip-bg: var(--drop-down-menu-bg); + --jf-palette-SnackbarContent-bg: var(--drop-down-menu-bg); + --jf-palette-SnackbarContent-color: var(--text); + --jf-palette-Button-inheritContainedBg: var(--button-color); + --jf-palette-Button-inheritContainedHoverBg: var(--button-color-hover); +} + +html[data-theme] { + background: var(--main-bg-color); + color: var(--text); +} + +html .MuiPaper-root { + color: var(--text); + background-image: none; +} + +html .MuiDrawer-paper { + background: var(--main-bg-color); + border-color: var(--transparency-light-15); +} + +html .MuiDrawer-docked .MuiDrawer-paper { + background: var(--transparency-dark-25); +} + +html .MuiAppBar-root { + background: var(--main-bg-color); + color: var(--text); +} + +html .MuiDialog-paper { + background: var(--modal-bg-color); +} + +html .MuiDialogTitle-root { + background: var(--modal-header-color); + color: var(--text-hover); +} + +html .MuiDialogActions-root { + background: var(--modal-footer-color); +} + +html .MuiMenu-paper, +html .MuiPopover-paper { + background: var(--drop-down-menu-bg); +} + +html .MuiButton-containedPrimary, +html .MuiButton-containedSecondary, +html .MuiButton-containedInherit { + --variant-containedBg: var(--button-color); + --variant-containedColor: var(--button-text); + background: var(--button-color); + color: var(--button-text); +} + +html .MuiButton-containedPrimary:hover, +html .MuiButton-containedSecondary:hover, +html .MuiButton-containedInherit:hover { + background: var(--button-color-hover); + color: var(--button-text-hover); +} + +html .MuiButton-root.Mui-disabled { + color: var(--text-muted); + background: var(--transparency-dark-15); +} + +html .MuiButton-root:focus-visible, +html .MuiIconButton-root:focus-visible, +html .MuiListItemButton-root:focus-visible { + outline: 2px solid rgb(var(--accent-color)); + outline-offset: 2px; +} + +html .MuiInputBase-root, +html .MuiInputLabel-root, +html .MuiFormLabel-root, +html .MuiListItemIcon-root { + color: var(--text); +} + +html .MuiInputLabel-root.Mui-focused, +html .MuiFormLabel-root.Mui-focused { + color: rgb(var(--accent-color)); +} + +html .MuiInputLabel-root.Mui-error, +html .MuiFormLabel-root.Mui-error { + color: var(--jf-palette-error-main); +} + +html .detailRibbon { + background: var(--transparency-dark-25); +} + +/* Late-loaded legacy styles use compiled colors rather than the jf palette. */ +.userProfilesPage .visualCardBox, +.dashboardDocument .paperList { + background-color: var(--card-background); +} + +/* Dashboard tables calculate their backgrounds outside the jf CSS palette. */ +.dashboardDocument .MuiPaper-root:has(> .MuiTableContainer-root), +.dashboardDocument .MuiPaper-root:has(> .MuiTableContainer-root) > .MuiBox-root, +.dashboardDocument .MuiTableRow-root, +.dashboardDocument .MuiTableCell-root, +.dashboardDocument .MuiMenu-list { + background-color: var(--drop-down-menu-bg); +} + +.dashboardDocument .MuiTableRow-root:hover .MuiTableCell-body { + background-image: linear-gradient(var(--transparency-light-10), var(--transparency-light-10)); +} 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/css/base/nzbget/nzbget-base.css b/css/base/nzbget/nzbget-base.css index 61b366c2a3..88de21fc61 100644 --- a/css/base/nzbget/nzbget-base.css +++ b/css/base/nzbget/nzbget-base.css @@ -46,7 +46,7 @@ body { .modal-header { padding: 9px 15px; border-bottom: 1px solid rgb(var(--accent-color)); - background: var(--modal-footer-color); + background: var(--modal-header-color); background-repeat: repeat, no-repeat; background-attachment: fixed, fixed; background-position: center center, center center; @@ -68,8 +68,13 @@ body { color: var(--text) !important; } +/* Let the modal background show through the native theme's body panel. */ +.modal-body { + background: transparent; +} + .modal-footer { - background-color: var(--modal-footer-color); + background: var(--modal-footer-color); background-repeat: repeat, no-repeat; background-attachment: fixed, fixed; background-position: center center, center center; @@ -240,6 +245,24 @@ div.check:hover { background-image: url(/resources/nzbget/icons.png); } +/* Keep checkbox positions aligned with the theme.park sprite in native dark mode. */ +table.table-cancheck tr.checked div.img-check { + background-position: -434px -18px; +} + +table.table-cancheck tr.checkremove div.img-check { + background-position: -402px -18px; +} + +/* The theme.park sprite has no separate hover row for the play/pause icons. */ +.PlayBlockInner:hover .img-download-btn-active { + background-position: -113px -80px; +} + +.PlayBlockInner:hover .img-download-btn-pause { + background-position: -177px -80px; +} + .navbar-search .search-query { color: var(--text); background: var(--transparency-dark-25) !important; @@ -444,7 +467,9 @@ div.check:hover { border-color: transparent; } -.btn:hover { +/* Match native dark mode's specificity for unselected default buttons. */ +.btn:hover, +.btn-default:hover:not(.btn-active) { color: var(--button-text-hover); background-color: var(--button-color-hover); diff --git a/css/base/ombi/ombi-base.css b/css/base/ombi/ombi-base.css index 7f27574735..7cf77f639a 100644 --- a/css/base/ombi/ombi-base.css +++ b/css/base/ombi/ombi-base.css @@ -38,6 +38,14 @@ html, body, .wizard-background, .content-container, #main-container\ dark > mat- background: transparent !important; } +.top-bar-container .profile-block a { + color: var(--text) !important; +} + +.top-bar-container .profile-block a:hover { + color: var(--text-hover) !important; +} + .container-alert { color: var(--text-hover); background: var(--transparency-dark-50) !important; @@ -233,13 +241,14 @@ a:hover { background: var(--transparency-light-10) !important; border: 1px solid rgb(255 255 255 / 10%) !important; border-radius: 30px; - color: var(--text-hover); + color: var(--text-hover) !important; margin-bottom: 10px; margin-right: 30px; } .discover-filter-buttons-group .button-active { - background: var(--transparency-dark-45) !important; + background: var(--button-color) !important; + color: var(--button-text) !important; } #search-filter{ @@ -491,6 +500,22 @@ button.admin-cog { } /* FORMS */ +.mat-hint { + color: var(--text-muted) !important; +} + +.mat-form-field-appearance-outline:not(.mat-form-field-invalid):not(.mat-form-field-disabled) .mat-form-field-label { + color: var(--text-muted) !important; +} + +.mat-form-field-appearance-outline.mat-focused:not(.mat-form-field-invalid) .mat-form-field-label { + color: var(--text-hover) !important; +} + +.mat-form-field-appearance-outline:not(.mat-focused):not(.mat-form-field-invalid):not(.mat-form-field-disabled) .mat-form-field-outline { + color: var(--text-muted); +} + ::ng-deep .dark .mat-form-field.mat-focused .mat-form-field-label, ::ng-deep .mat-form-field.mat-focused .mat-form-field-label { color: rgb(var(--accent-color)) !important; @@ -1588,4 +1613,4 @@ hr { .profile-link:hover .profile-avatar { box-shadow: 0 0 0 2px rgba(var(--accent-color), 0.4) !important; -} \ No newline at end of file +} diff --git a/css/base/pihole/pihole-base.css b/css/base/pihole/pihole-base.css index ecbeed5d3f..3a8eea6858 100644 --- a/css/base/pihole/pihole-base.css +++ b/css/base/pihole/pihole-base.css @@ -259,6 +259,15 @@ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); } + .box, + .box-solid, + .box-header.with-border, + .box-footer, + .content .page-header, + .content hr { + border-color: var(--transparency-light-15); + } + .box-solid>.box-header, .box>.box-header { color: var(--text); @@ -291,7 +300,8 @@ } .table-bordered { - background: var(--transparency-dark-10) + background: var(--transparency-dark-10); + border-color: var(--transparency-light-15); } .table-bordered>thead>tr>th, @@ -411,6 +421,27 @@ } /* Network */ + /* Pi-hole's row renderer parses these as opaque rgb() colors. */ + .network-never { + background-color: #661b02; + } + + .network-recent { + background-color: #114100; + } + + .network-old { + background-color: #525200; + } + + .network-older { + background-color: #502b00; + } + + .network-gradient { + background-image: linear-gradient(to right, #114100 0%, #525200 100%); + } + .table-striped>tbody>tr:nth-of-type(odd):not(#network-details .table-striped>tbody>tr:nth-of-type(odd)) { background: var(--transparency-dark-25) !important; } diff --git a/dev/.gitignore b/dev/.gitignore new file mode 100644 index 0000000000..840b52afe9 --- /dev/null +++ b/dev/.gitignore @@ -0,0 +1,3 @@ +/artifacts/ +**/__pycache__/ +**/.env diff --git a/dev/README.md b/dev/README.md new file mode 100644 index 0000000000..f699b43558 --- /dev/null +++ b/dev/README.md @@ -0,0 +1,315 @@ +# Local theme development + +Use the setups in this folder to reproduce CSS bugs and develop app themes. +Follow the [theme.park setup guide](https://docs.theme-park.dev/setup/) and the +app's instructions in [tp-docs](https://github.com/themepark-dev/tp-docs). +Record the local startup commands and verification steps in `dev//README.md`. + +## Current setup + +The [NZBGet setup](nzbget/README.md) uses Compose to run the official app Docker +mod with local CSS. Its seed script creates paused dummy downloads. + +[serve.py](serve.py) uses Python's standard HTTP server and adds +`Cache-Control: no-store`. The Compose service mounts the checkout's CSS and +resources read-only. File edits appear on refresh without a build. + +There is no shared nginx proxy or theme.park image setup yet. Add and test one +when an app task needs it, using the guidance below. + +The [Dozzle setup](dozzle/README.md) runs synthetic container logs and injects +CSS through a local nginx proxy. It includes Dozzle's CSP and streaming API +exceptions, with a direct port for native comparisons. + +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. + +The [Jellyfin setup](jellyfin/README.md) uses synthetic movies, built-in Custom CSS, +and nginx subfiltering for the dashboard. Its notes cover the Jellyfin 12 palette +and table backgrounds that do not follow the app's CSS variables. + +## 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. +The theme.park image handles hosting. The app still needs a way to load the CSS. + +| CSS host | When to use it | +| --- | --- | +| Local Python server | Edit source CSS and refresh the app. Load the app base CSS followed by the chosen theme option. | +| theme.park Docker image | Check packaged files, generated theme URLs, startup, or subfolder handling. Build from the checkout to test local edits. | + +The documented image is `ghcr.io/themepark-dev/theme.park`. See the +[Docker setup](https://docs.theme-park.dev/setup/#docker) for tags, ports, +configuration, and `TP_URLBASE`. A pulled release image does not contain edits +in this checkout. + +The current [image startup script](../docker/root/etc/s6-overlay/s6-rc.d/init-themepark/run) +copies bundled files from `/app/themepark` into `/config/www`, runs `themes.py`, +and changes configuration ownership. Use disposable configuration when testing +the image. Do not mount the working checkout at `/config/www`. Startup can +overwrite its files or change their ownership. Rebuild after editing files, +or set up and verify a way to copy those edits into the test container. + +The image's [nginx configuration](../docker/root/defaults/nginx/site-confs/default.conf) +disables caching and rewrites asset paths for its subfolder endpoint. +The Python server does neither subfolder rewriting nor theme generation. +When editing source CSS, link the base stylesheet and theme option separately. +To test a generated `/.css` URL, generate wrappers in a temporary +directory or use the image. Confirm the browser loads every imported file. + +| Documentation marker | Injection method | +| --- | --- | +| 🐳 | LinuxServer Docker mod, configured to load the local CSS host. | +| 🔥 | Documented Hotio/S6 startup script mount; do not assume `DOCKER_MODS` works. | +| ⚙️ | The app's built-in CSS setting, using locally served styles. | +| No method marker | Local reverse proxy using the subfiltering guide. | +| ⚠️ | Read the app-specific setup requirements in addition to the chosen method. | + +Reproduce the reporter's installation method where supported. You can try +styles in the browser during investigation. Verify the final files through +the chosen installation method after reloading the page. + +## Local reverse proxy + +Use an nginx container on the application's Compose network. The browser opens +the proxy's loopback-bound port. Nginx forwards requests to the app's service +name and internal port. Stylesheet links must use an address the browser can +reach. Docker service names usually resolve only inside Docker networks. +Inside the nginx container, `localhost` refers to that container. + +Follow the [nginx subfilter recipe](https://docs.theme-park.dev/setup/#nginx): +request uncompressed upstream content, inject once, and keep injection out of +API locations. Start with the app and CSS host on separate ports +so their `/css` and `/resources` paths do not collide. + +Make the injection point and proxy exceptions app-specific. For example, +[Sonarr](https://docs.theme-park.dev/themes/sonarr/) documents `` injection. +Other apps may need CSP adjustments or a separate unbuffered API location. +Apply exceptions only where required by the app's documented setup. Verify +login, redirects, and stylesheet order in the running app. Check streaming and +WebSockets if the app uses them. Use the app's documented injection point. + +## Workflow for an app task + +Start a task branch from freshly fetched `origin/develop` and merge the finished +work back into `develop`. Use `testing` only for potentially breaking changes +that could disrupt users of `develop`. Ordinary fixes do not need to pass +through `testing`; disruption for users of that branch is accepted. +The maintainer's explicit authorization is still required before pushing or +publishing changes. + +1. Read the report and docs. Record the app version, injection method, theme, + browser, viewport, and steps needed to reach the problem. +2. Reuse `dev/`. If absent, add a minimal Compose file and README. Record + the image tag, local ports, dependencies, setup and login steps, injection + method, and commands to stop or reset the app. Check occupied ports before starting. +3. Give the test instance its own configuration. Add dummy data or a seed script + when an empty installation cannot display the affected screen. Keep real + credentials and private data out of tracked files. If an app needs an + existing instance or user-provided access, record that requirement. +4. Confirm local CSS and assets load, reproduce the bug, and capture the + original appearance before editing. Record unrelated existing defects. +5. Inspect DOM and computed styles, compare upstream changes when useful, and + make the smallest appropriate source change using theme variables. +6. Reload through the real injection method. Exercise relevant states such as + hover, focus, selection, dialogs, and empty/populated views. Check native + light/dark modes where supported. Use the background checks below to choose + theme options, plus a light community palette when relevant. Include the reported browser and + responsive layout where practical. Record any substitutions. +7. Inspect screenshots as well as computed styles. For shared CSS changes, + check other apps that import it. Run relevant generation and build checks + in a temporary directory. +8. Record the cause, patch, image tag, app version, browser, + theme, viewport, reproduction steps, evidence, and gaps. Store screenshots + 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. 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 + the maintainer's explicit instruction. + +For a new theme.park application, also add the app base stylesheet, +any required resources, and the appropriate documentation/navigation entries in +`tp-docs`. Follow [CONTRIBUTING.md](../.github/CONTRIBUTING.md), including screenshots +of all official theme options. Adding a local environment for an already +supported app does not require adding another application theme. + +## Older issues and UI replacements + +Record the issue date and reported app version separately. When the report +omits a version, do not infer an exact release from its date or screenshot. +Check the current stable release, its UI migration notes, and the history of +the app's base CSS before writing selectors for an old screenshot. + +Run the current app with its native CSS and with the existing theme. Compare +the affected component plus other main screens. Distinguish a missing selector +from a replaced component system. Inspect the current DOM, framework, and color +variables. Zero matches for old selectors are evidence for the inspected +screens, not proof that those selectors are unused on every supported version. + +If the UI was broadly rebuilt, report whether support needs a theme rebuild +instead of a small bug fix. Explain the broken areas, reusable styling hooks, +and verification scope. Let the maintainer decide whether that investment fits +continued support. Do not silently deprecate the app or present a partial +sidebar patch as restored support. Keep the working local setup and findings +available for that decision. + +## Background checks + +Test background types as well as colors. Use at least these cases when changing +backgrounds, transparency, dialogs, or panels: + +| Theme | What it checks | +| --- | --- | +| Nord | Solid colors, including distinct modal body and header/footer colors. | +| Aquamarine | A radial page gradient and a linear modal gradient. | +| Hotline | A linear page gradient with a different direction from its modal gradient. | +| Plex, when layers matter | Several page gradients with a solid fallback color. | + +Read the component's variables before testing. A radial page background does +not imply a radial modal background. If no existing option exercises the value +needed for a check, use a temporary custom option and label it in the results. + +Variables such as `--main-bg-color` and `--modal-bg-color` can contain a full +background declaration, including an image, position, size, repeat, and +attachment. Use `background` to accept that value. `background-color` accepts +only a color, while `background-image` cannot accept the extra background +settings. A declaration with an incompatible variable can fail after variable +substitution even though the browser parsed the stylesheet. + +Inspect the visible element and its children. A solid child panel can cover the +correct gradient on its parent. Compare the dialog shell, body, header, and +footer separately. The header, body background, and footer can each have a +different color or gradient. Verify each against its own variable; matching +colors are not a requirement. If a theme gives them identical values, test with +three distinct temporary values to expose accidental variable substitutions. +Check text contrast and borders in each section. +Resize and scroll the dialog to catch gradient seams, repeated backgrounds, +or transparent areas that reveal the page instead of the dialog. +Check which element actually scrolls. A phone layout may scroll the document +instead of the modal body. Confirm that the scroll position changed. + +Wait for stylesheet loads, fonts, and modal animations before taking screenshots. +When switching themes in the browser, reload the final setup through its normal +injection method as well. Inspect the screenshots after the computed-style checks. + +For hover bugs, move a real browser pointer onto the control. Dispatching a +`mouseover` event from JavaScript does not activate CSS `:hover`. Compare the +background and text before hover, during hover, and after moving away. Inspect +the winning selectors, including classes inside `:not()`, before adding +specificity or `!important`. Check other uses of the same button class and keep +selected, disabled, and status-colored controls distinguishable. +Include custom controls such as clickable icon containers in the inspection; +an app's shared `.btn` rules may not cover them. For sprite icons, check both +the image URL and the rectangle selected by each state's background position. + +## Defects found while investigating + +App updates often affect more than the component named in an issue. Inspect +nearby controls and at least one other use of a shared component. Keep a short +record of each additional defect, its reproduction steps, and whether it is +part of the current fix. Tell the maintainer about remaining defects rather +than treating them as covered by the original issue. + +Save reusable navigation and debugging notes in the app README. Keep screenshots +and detailed results in the ignored artifact directory. A useful note names the +element, the rule that caused the problem, and the checks that exposed it. + +## Keep lessons for the next session + +Document discoveries that would save another session time or prevent a repeated +mistake. Apply the `unslop` skill to every note. Put shared workflow lessons here +and app-specific setup, navigation, selectors, and exceptions in +`dev//README.md`. Keep `AGENTS.md` focused on instructions for doing the work. +Update an existing note when it covers the same subject. + +A useful note explains the observed problem, its cause, and the command or +interaction that resolves it. Include the app version when behavior may change +between releases. State what was verified and label untested ideas. Record +maintainer-approved exceptions with their reason and scope so another session +does not undo them. Keep temporary paths, full logs, and individual run results +under ignored `dev/artifacts///`. Do not copy a session transcript +into the README or include credentials and private data. + +### Preserve the review instance + +When the maintainer is checking a running instance, preserve its selected theme +and profile settings. Separate browser contexts may still share settings stored +by the app on its server. Dozzle shares its profile between the native and proxy +URLs, so automated appearance changes can affect the maintainer's tabs. + +Check Compose defaults before recreating services. A theme selected with an +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 +for screenshots that still show the old UI. Reuse verified captures of the final +CSS, with synthetic data and consistent viewport sizes. + +Inspect the page template and `mkdocs.yml` before replacing assets. Dozzle's +gallery loops over `config.extra.themes` and expects +`docs/site_assets/dozzle/.png`. Existing files can be missing options +that the gallery already references. Compare the configured list with the +captures instead of replacing only the images already present. + +Build MkDocs, open the rendered app page, and check that every gallery image +loads and shows the intended theme. A successful build alone does not establish +that image URLs work. Keep the docs change in a separate branch and PR targeting +`develop`; publishing still requires the maintainer's instruction. + +## Publishing development files + +The Pages workflow publishes the repository root and excludes `dev` and +`AGENTS.md` from all three deployment branches. This also excludes local +artifacts nested under `dev`. Keep those exclusions when changing deployment +steps; Git ignore rules alone do not control the published files. +The application Dockerfiles copy specific asset paths, which exclude these +development files. Keep tooling changes separate from CSS fixes for review. + +## Component libraries and CSS modules + +Bazarr's Mantine update replaced the old `bazarr-*` classes, but retained public +classes such as `mantine-Button-root`. Check the app's theme provider and rendered +DOM before treating generated CSS-module names as a support blocker. Prefer +public component classes, state attributes and semantic containers. Document any +module-name prefix you still need in the app README. + +Check for collisions between theme variables and the component library's local +variables. Bazarr's `--button-color` means text color in Mantine and background +color in theme.park. A theme alias resolved at the document root avoids the +component's local override. Test normal, hover and disabled states after changing +variable mappings, and preserve explicit status colors. diff --git a/dev/bazarr/README.md b/dev/bazarr/README.md new file mode 100644 index 0000000000..b1d3b12b3a --- /dev/null +++ b/dev/bazarr/README.md @@ -0,0 +1,152 @@ +# Bazarr CSS development + +Run commands from the repository root. This setup uses LinuxServer's image and +[the documented Docker mod](https://docs.theme-park.dev/setup/#docker-mods). +Follow the [Bazarr theme instructions](https://docs.theme-park.dev/themes/bazarr/) +and select Bazarr's native dark appearance in Settings > UI. + +## Start and populate + +```sh +docker compose -f dev/bazarr/compose.yaml up -d +``` + +Wait for Bazarr to start at . The mod injects stylesheets +from , served directly from this checkout. Refresh after +editing `css/base/bazarr/bazarr-base.css`. No SSH or remote CSS host is needed. +The browser must be on the machine that publishes these loopback ports. + +After the first startup, populate the disposable configuration volume: + +```sh +docker compose -f dev/bazarr/compose.yaml stop bazarr +docker compose -f dev/bazarr/compose.yaml run --rm seed +docker compose -f dev/bazarr/compose.yaml start bazarr +``` + +The fixture enables the series/movie pages and dark appearance, disables +analytics, and adds 35 series, 105 episodes, 35 movies, subtitle history and an +English/Norwegian profile. It creates no media files and connects to no Sonarr, +Radarr or subtitle-provider account. Connection health warnings are expected. +Do not configure real services in this instance. Run the seed only while Bazarr +is stopped and only against this development volume. It refuses libraries with +paths outside `/themepark-fixtures/`. + +## Versions, themes and add-ons + +The default image tag is `latest`, matching the maintainer's installation. +Record the resolved build before testing because the tag changes: + +```sh +docker image inspect lscr.io/linuxserver/bazarr:latest --format '{{index .Config.Labels "build_version"}} {{index .RepoDigests 0}}' +``` + +Set `BAZARR_TAG=version-v1.4.4` to test the first release reported broken in +[issue 658](https://github.com/themepark-dev/theme.park/issues/658). Use a fresh +configuration volume for each older version. Do not downgrade a newer database. +The seed supports the subtitle tables in both 1.4.4 and 1.6.0. + +Maroon is the default theme because it exposes partial theming clearly. +For another theme or the optional add-ons: + +```sh +TP_THEME=nord docker compose -f dev/bazarr/compose.yaml up -d bazarr +TP_ADDON='bazarr-darker|bazarr-4k-logo' docker compose -f dev/bazarr/compose.yaml up -d bazarr +``` + +Use `TP_COMMUNITY_THEME=true` with a name from `css/community-theme-options`. +Keep the same environment values on later `up` commands if you want to retain +them. Browser-only theme switching is useful for comparisons; reload through +the normal mod injection for the final check. + +## Inspection paths + +- Series and Movies: table links, subtitle badges, row hover, pagination, wrench + editor, Mass Edit checkboxes and profile selector. The fixture spans two pages. +- Series > Sample series 01: episode groups, missing/present subtitles, Edit + Series, Upload, manual search and episode history dialogs. +- Movies > Sample movie 01: missing subtitles and Edit Movie. Sample movie 02 + also has an existing subtitle for subtitle tools and deletion confirmation. +- Settings > Languages: multi-select pills, Add New Profile, validation errors, + Add Language and switches. Profile removal changes the staged settings without + a confirmation dialog; leave the page without saving after checking it. +- Settings > Providers: click the plus card, select OpenSubtitles.com, inspect + the password field and switches. No account credentials are needed for this. +- History > Statistics: chart axes, legend and tooltip. Preserve series/movie + colors. System > Status, Tasks and Logs exercise other tables and messages. +- Header: search suggestions, system menu and Jobs Manager drawer. Opening the + menu is sufficient; do not trigger restart/shutdown to test its styling. + +Check Nord, Aquamarine, Hotline and Plex, plus Maroon from the issue. For broad +component changes, check every built-in palette. Compare modal body, header and +footer against their separate variables. Temporarily give those three variables +different values if the selected palette uses identical ones. +On phones, test the navigation overlay and scroll the language-profile dialog +to its Save button. Confirm the scroll container actually moved. + +## Findings from issue 658 + +Bazarr 1.4.4 uses Mantine 7; 1.6.0 uses Mantine 9. Both expose stable +`mantine-Component-part` classes even though Bazarr's custom styles have generated +module names. Prefer the public classes and state attributes. The old +`bazarr-*` rules remain for pre-1.4.4 installations. + +The toolbar and provider card lack public classes of their own. Their overrides +match the `_group_` or `_card_` module-name prefix within the main area. Recheck +those two selectors when Bazarr changes the component source. Do not copy hash +suffixes such as `_13q4v_32` into production CSS. + +Mantine defines `--button-color` locally for text, colliding with theme.park's +background variable. Resolve the theme's value at the document root into +`--bazarr-button-background`, then use that alias on buttons. Inspect both the +variable's value and where it is defined before diagnosing a transparent or +white button. Check real hover, disabled controls and red destructive actions. + +Keep warning, highlight and disabled subtitle badges distinct. Inline status +colors on action icons must survive the generic icon rules. Mantine's native +display rules also expose some controls marked `hidden`, including the new +provider's Disable button and the zero-change badge. The base CSS restores the +hidden state for those controls. + +Most dialogs have a content shell and a header. Item editors and provider +settings put their footer actions in the final Group inside the body Stack. +The language-profile dialog ends with a standalone Save button instead. Avoid +styling every Group or every final button as a modal footer. + +Save screenshots, DOM captures, downloaded upstream source and detailed check +results under `dev/artifacts/bazarr//`. Keep these out of commits. +Actual subtitle downloads, authentication and external integrations require +separate functional checks; the fixture covers their available UI controls. + +## Stop or discard + +```sh +docker compose -f dev/bazarr/compose.yaml down +``` + +Add `--volumes` only when discarding this disposable database. CSS edits remain +in the checkout. + +## Validation on 2026-09-14 + +LinuxServer `latest` resolved to Bazarr `1.6.0-ls363`, built 2026-09-08. +Bazarr 1.4.4 was checked in a separate container and configuration volume. +Both used the real Docker mod with local CSS and native dark appearance. + +Firefox 155 passed checks for all 11 built-in palettes on both versions, +including header backgrounds, button text/background/hover, modal body, header, +footer and dropdown backgrounds. Screenshots were inspected for contrast and +layout. Chromium 152 in the T3 preview was also checked on series details and +the editor modal. + +The current version also passed disabled-button hover, validation errors, +checkbox selection, pagination, provider fields, upload metadata, manual-search +layout, chart tooltips and separate temporary modal colors. The 390px phone +layout had no document-width overflow; navigation and a scrolled profile editor +were checked. Both add-ons loaded through Docker-mod injection. The production +`minify@7.2.2` output passed the Maroon button/modal checks. + +No subtitle download, upload submission, real media scan or external account +connection was performed. Pre-1.4.4 selectors were retained but that frontend +was not retested. Native light appearance and every community palette were not +part of this check. diff --git a/dev/bazarr/compose.yaml b/dev/bazarr/compose.yaml new file mode 100644 index 0000000000..5ed2f76bda --- /dev/null +++ b/dev/bazarr/compose.yaml @@ -0,0 +1,37 @@ +name: themepark-bazarr-dev +services: + css: + image: python:3.12-alpine + working_dir: /site + command: ["python", "/serve.py"] + ports: ["127.0.0.1:18965:8000"] + volumes: + - ../../css:/site/css:ro + - ../../resources:/site/resources:ro + - ../serve.py:/serve.py:ro + bazarr: + image: lscr.io/linuxserver/bazarr:${BAZARR_TAG:-latest} + depends_on: [css] + environment: + PUID: "1000" + PGID: "1000" + TZ: UTC + DOCKER_MODS: ghcr.io/themepark-dev/theme.park:bazarr + TP_DOMAIN: localhost:18965 + TP_SCHEME: http + TP_THEME: ${TP_THEME:-maroon} + TP_ADDON: ${TP_ADDON:-} + TP_COMMUNITY_THEME: ${TP_COMMUNITY_THEME:-false} + ports: ["127.0.0.1:16767:6767"] + volumes: + - config:/config + seed: + image: python:3.12-alpine + profiles: [tools] + user: "1000:1000" + command: ["python", "/seed.py"] + volumes: + - config:/config + - ./seed.py:/seed.py:ro +volumes: + config: diff --git a/dev/bazarr/seed.py b/dev/bazarr/seed.py new file mode 100644 index 0000000000..9df7f208a9 --- /dev/null +++ b/dev/bazarr/seed.py @@ -0,0 +1,75 @@ +"""Populate only the disposable Compose volume, with Bazarr stopped.""" +import datetime +import json +import re +import sqlite3 +from pathlib import Path + +config = Path('/config/config/config.yaml') +s = config.read_text() +for key, value in {'theme': 'dark', 'use_sonarr': 'true', 'use_radarr': 'true'}.items(): + s, count = re.subn(rf'^ {key}: .*$', f' {key}: {value}', s, flags=re.M) + assert count == 1, key +s = re.sub(r'(analytics:\n enabled:) true', r'\1 false', s) +db = sqlite3.connect('/config/db/bazarr.db') +# Refuse to alter a library that was not created by this fixture. +for table, column in [('table_shows', 'path'), ('table_movies', 'path')]: + if db.execute(f"SELECT count(*) FROM {table} WHERE {column} NOT LIKE '/themepark-fixtures/%'").fetchone()[0]: + raise SystemExit('Refusing to seed an existing library') + +config.write_text(s) + +def insert(table, **row): + available = {r[1] for r in db.execute(f'PRAGMA table_info({table})')} + if not available and table.endswith('_subtitles'): + parent = 'table_episodes' if table == 'table_episodes_subtitles' else 'table_movies' + key = 'sonarrEpisodeId' if parent == 'table_episodes' else 'radarrId' + db.execute(f'UPDATE {parent} SET subtitles=? WHERE {key}=?', + (repr([[row['language'], row['path'], row['size']]]), row[key])) + return + row = {k: v for k, v in row.items() if k in available} + if 'subtitles' in available: + row.setdefault('subtitles', '[]') + columns = ','.join(f'"{k}"' for k in row) + db.execute(f'INSERT OR REPLACE INTO {table} ({columns}) VALUES ({",".join("?" for _ in row)})', list(row.values())) + +insert('table_languages_profiles', profileId=1, name='English + Norwegian', cutoff=None, + originalFormat=0, items=json.dumps([ + dict(id=1, language='en', hi='False', forced='False', audio_exclude='False'), + dict(id=2, language='nb', hi='False', forced='False', audio_exclude='False')]), + mustContain='[]', mustNotContain='[]') +db.execute("UPDATE table_settings_languages SET enabled=1 WHERE code2 IN ('en','nb')") +now = datetime.datetime.now() +for i in range(1, 36): + title = f'Sample series {i:02}' + common = dict(title=title, sortTitle=title.lower(), path=f'/themepark-fixtures/series/{i}', + profileId=1, audio_language="['English']", tags='[]', alternativeTitles='[]', + monitored='True' if i % 3 else 'False', year='2025', poster='', fanart='', + overview='Disposable theme.park sample data. No media files or external services are connected.') + insert('table_shows', sonarrSeriesId=i, tvdbId=i, seriesType='standard', ended='False', **common) + for j in range(1, 4): + eid = i * 10 + j + path = f'{common["path"]}/S01E{j:02}.mkv' + insert('table_episodes', sonarrEpisodeId=eid, sonarrSeriesId=i, season=1, episode=j, + title=f'Sample episode {j}', path=path, audio_language="['English']", monitored='True', + missing_subtitles="['en', 'nb']" if j == 1 else "['nb']" if j == 2 else '[]', + file_size=1500000000, resolution='1080p', video_codec='h264', audio_codec='aac', format='WEB-DL') + if j > 1: + insert('table_episodes_subtitles', id=eid, language='en', hi=False, forced=False, + path=path.replace('.mkv','.en.srt'), size=12000, sonarrEpisodeId=eid, sonarrSeriesId=i) + insert('table_history', id=eid, action=1, description='English subtitles downloaded for the sample episode', + language='en', provider='opensubtitlescom', score=345, score_out_of=360, + sonarrEpisodeId=eid, sonarrSeriesId=i, timestamp=str(now-datetime.timedelta(days=i%7)), + video_path=path, subtitles_path=path.replace('.mkv','.en.srt'), matched="['series', 'season', 'episode']", not_matched="['release_group']") + common.update(title=f'Sample movie {i:02}', sortTitle=f'sample movie {i:02}', path=f'/themepark-fixtures/movies/{i}.mkv') + insert('table_movies', radarrId=i, tmdbId=str(i), missing_subtitles="['en', 'nb']" if i%2 else '[]', + file_size=3000000000, resolution='1080p', video_codec='h264', audio_codec='aac', format='BluRay', **common) + if not i%2: + insert('table_movies_subtitles', id=i, radarrId=i, language='en', hi=False, forced=False, + path=common['path'].replace('.mkv','.en.srt'), size=14000) + insert('table_history_movie', id=i, radarrId=i, action=1, description='English subtitles downloaded for the sample movie', + language='en', provider='opensubtitlescom', score=110, score_out_of=120, + timestamp=str(now-datetime.timedelta(days=i%7)), video_path=common['path'], + matched="['title', 'year']", not_matched="['release_group']") +db.commit() +print('Seeded 35 series, 105 episodes, 35 movies, history and a language profile.') diff --git a/dev/dozzle/README.md b/dev/dozzle/README.md new file mode 100644 index 0000000000..c5771375b4 --- /dev/null +++ b/dev/dozzle/README.md @@ -0,0 +1,212 @@ +# Local Dozzle theme development + +This setup was added for [issue #559](https://github.com/themepark-dev/theme.park/issues/559). +The report dates from April 28, 2024 and shows Dracula with barely visible +stack labels. It does not name an app version or injection method. + +The refresh targets Dozzle v11.0.1. Its +[UI redesign notes](https://dozzle.dev/guide/whats-new) cover the sidebar, +dashboard, log viewer, settings, and menus. The maintainer explicitly excluded +backward compatibility from this task, so the base stylesheet replaces the old +Bulma/Buefy rules. Older Dozzle releases are not supported by this refresh. + +## Start and compare + +Run from this checkout's root: + +```sh +docker compose -f dev/dozzle/compose.yaml up -d +``` + +| Address | Purpose | +| --- | --- | +| http://localhost:18080 | Native Dozzle without theme.park injection. | +| http://localhost:18081 | Same app through nginx with theme.park CSS. | +| http://localhost:18865 | Live CSS and resources from this checkout. | + +All published ports bind to loopback. No login is configured. Dozzle reads the +local Docker socket and filters its UI to containers labeled +`dev.themepark.sample=dozzle`. The filter limits what it displays, not socket +permissions. A read-only socket mount does not restrict Docker API operations. +Container actions and shell access are disabled. Analytics is disabled and the +instance is not linked to Dozzle Cloud. + +The two running samples emit synthetic JSON logs with debug, info, warning, and +error levels, nested fields, multiline text, and ANSI colors. Display-name labels +keep their names short without changing their Compose stack group. `sample-stopped` exits with code 1 intentionally, providing an +exited container to inspect. It is not a setup failure. The samples share a +Compose project, which produces the sidebar stack group without external data. + +For development, edit `css/base/dozzle/dozzle-base.css` and refresh port 18081. +The CSS server mounts source files read-only and sends `Cache-Control: no-store`. +There is no image rebuild needed for CSS edits. The default theme is Dracula. +To change the injected option: + +```sh +TP_THEME=aquamarine docker compose -f dev/dozzle/compose.yaml up -d proxy +TP_THEME=catppuccin-latte TP_THEME_FOLDER=community-theme-options docker compose -f dev/dozzle/compose.yaml up -d proxy +``` + +`DOZZLE_TAG` can select another release. Record the exact version tested; this +setup has only been verified with v11.0.1. Recreate the proxy after replacing +Dozzle because nginx resolves the upstream service at startup: + +```sh +DOZZLE_TAG=v11.0.1 docker compose -f dev/dozzle/compose.yaml up -d dozzle +docker compose -f dev/dozzle/compose.yaml up -d --force-recreate proxy +``` + +## Injection and streaming + +The proxy follows the [theme.park setup guide](https://docs.theme-park.dev/setup/#nginx) +and [Dozzle-specific instructions](https://docs.theme-park.dev/themes/dozzle/): + +- Request uncompressed HTML and insert the base CSS and theme option before + ``. Confirm each link occurs once in the response. +- Remove upstream `Content-Security-Policy` and `X-WebKit-CSP` headers on the + themed route, as documented for Dozzle. The direct port retains native CSP. +- Keep `/api` separate with buffering and caching off for live event/log streams. + Do not inject anything into those responses. +- Use a separate CSS origin so absolute imports such as `/css/defaults/transparent.css` + resolve to theme.park files instead of colliding with application routes. + +Test a newly arriving heartbeat as well as existing log rows. Seeing old logs +does not verify that the proxy streams updates. The heartbeat message repeats, +so compare the timestamp in the same rendered row rather than the message text +alone. This setup does not enable or +validate shell/attach WebSockets, authentication, a URL subfolder, Swarm, +Kubernetes, or remote agents. + +## Navigation and inspection + +1. On the homepage, expand and collapse the Compose group in the sidebar. + Open its More options menu and enable Show all containers to include the + stopped sample. Alternatively use the corresponding setting on `/settings`. +2. Select sample-web to open the log viewer. Inspect selected and hovered rows, + the group label, timestamps, log severity, the toolbar, and stream background. +3. Open `/settings` directly or use the homepage gear. The gear is not present + in every log-view layout. Compare controls, labels, and section backgrounds. +4. Change native appearance separately from the injected theme. v11 stores + profile settings on the server, shared by the direct and proxy URLs. A + native mode switch can affect other open tabs. Automated comparisons should + set the profile before opening a fresh page to avoid reload races. +5. At phone width, use `[data-testid="hamburger"]` to open the sidebar. The + desktop and mobile layouts use the same SideMenu component. + +Useful v11 selectors are `[data-testid="side-menu"]`, `.nav-group-toggle`, +`.nav-item`, `.nav-item.is-active`, and `.btn`. The historical `.menu-list` +selector does not reach this sidebar. Read upstream `assets/main.css`, +`assets/components/nav/NavGroup.vue`, and `NavItem.vue` before adding overrides. +Avoid copying Vue's generated `data-v-*` attributes into new theme rules. + +Save screenshots, source snapshots, and browser results under +`dev/artifacts/dozzle/559/`. Browser versions, screenshots, and run results are stored in that ignored +directory. Do not add upstream source checkouts or screenshots to the CSS host +or the tracked fixture. + +## Styling hooks and pitfalls + +Dozzle 11 uses Tailwind 4 and DaisyUI 5. Prefer public component classes and +attributes over Vue's generated `data-v-*` attributes. The base stylesheet maps +DaisyUI colors at `html[data-theme]`, so theme.park controls the palette in both +native light and dark modes. The native choice still controls browser appearance. + +- Keep `--color-base-*` values color-only. They feed `color-mix()` and utility + classes. Use full `background` declarations for page gradients, floating menus, + mobile navigation, sticky log headers, and dialogs. A transparent base color + needs explicit opaque backgrounds wherever content floats over logs. +- The search dialog has a padded, transparent `.modal-box` around its visible + panel. Theme that inner panel and its header/footer separately. Ordinary + `.modal-box` elements and the `.modal-right` drawer need their own background. + Notification form actions use `.sticky.bottom-0` inside the drawer. + Drawer headers are inset within the body, without an edge-to-edge divider. + Keep them transparent so the modal background continues through the heading. + The maintainer explicitly approved skipping `--modal-header-color` here after + gradient themes exposed a separate rectangle inside the modal padding. + The search dialog still uses its separate header and footer variables. +- `.nav-group-toggle` is the stack label from issue #559. `.nav-item.is-active` + and `.is-merged` mark selected streams. Use readable text with an accent border + and tint; dark accent colors alone can disappear against gradient backgrounds. +- DaisyUI status buttons expose `--btn-color` and `--btn-fg`. Preserve those for + danger/status controls. Dozzle's generic hover rule otherwise replaces their + backgrounds with `--color-base-100`. Disabled buttons use `pointer-events: none`. +- Expression errors use the literal class `input-error!`. Its error border must + beat the normal `:focus-within` border. CodeMirror exposes `.cm-editor` and + semantic color variables, so generated syntax-token classes are unnecessary. +- ANSI log colors use `--ansi-*`. Native light-mode colors can become too dark + after injecting a dark theme. The theme mixes terminal hues with its text color + and reuses them for JSON and editor highlighting. Log severity indicators retain + Dozzle's error, warning, info, and debug colors. +- Theme changes animate text and buttons. Wait for link loading and transitions + before checking exact colors or capturing screenshots. +- Log lists expose `data-logs`. Use that attribute for their darker transparent + background, not `highlight-errors`, which is an optional row-highlighting + setting. Keep row hover and severity tints visible over the list background. + The details drawer's `.field-row code` values stay transparent; the general + inline-code background otherwise paints a separate box behind each value. + +## Interaction paths + +| Area | Steps | +| --- | --- | +| Container search | Home, Search containers. Type a nonexistent name for the empty state. Escape closes it. | +| Log details | Open sample-web, hover an error row, then its ellipsis, then Show details. Inspect nested JSON and field toggles. | +| Settings dropdown | Settings, one of the Auto dropdowns. Check the open menu and selected option. | +| Notification form | Notifications, Add alert. Try the log, metric, and event types. Type `name ???` into the container expression to see validation. | +| Discard confirmation | Change an alert field, then Cancel. Keep editing or Discard affects only the unsaved form. | +| Destination form | Notifications, Destinations tab, Add destination. Inspect the payload editor without testing or saving a webhook. | +| Phone menu | At 390 by 844, open the hamburger, select sample-web, and open log details. Scroll the actual drawer to reach its lower fields. | + +Notifications uses buttons with `role="tab"` for Alerts and Destinations. Locate +those as tabs in browser automation. The discard button's English name is +`Discard`, and the disabled form action is `Create Alert`. + +Log action menus close 150 ms after the pointer leaves them. Move a real pointer +into the open panel before waiting for its animation and clicking an item. +Otherwise an automation stability wait can outlast the hover menu. For phone +checks, emulate touch as well as viewport width. Dozzle uses a click toggle when +`hover: hover` is false. Wait for initial log loading to settle before opening a +row menu; automatic scrolling can move its anchor. A long-running sample can +push the initial ANSI/nested examples outside the first log batch. Use the +stopped sample for fixed data, load older logs, or recreate only the synthetic +sample services when fresh captures are needed. + +## Verification for issue #559 + +The September 2026 refresh was checked on `amir20/dozzle:v11.0.1`, digest +`sha256:8d88ee5cb7f7144bc5d42f11181e576a321d5d084101c0ae3db7227a8d84967e`. +The report is from April 2024 and names neither a version nor an injection method. +The refresh intentionally targets the current UI, not the historical screenshot. + +Firefox 155.0 checks cover all 11 official theme options in both native modes, +plus Catppuccin Latte. They include dashboard, stack labels, button hover, +container search, logs, and the details drawer. Nord, Aquamarine, and Hotline +also have 390 by 844 touch checks, including drawer scrolling and page overflow. +Chromium 152.0.7977.65 in T3 Code was checked on the dashboard and search dialog. + +Additional checks cover toggles, keyboard focus, empty search, invalid expression +borders, disabled buttons, discard-button hover, notification forms, and a fresh +heartbeat arriving through nginx. A temporary option also checks a radial modal body, a solid header, and a +linear footer with distinct colors. CSS injection, imports, served file bytes, +nginx configuration, and production minification are checked separately. + +The PR for this refresh must include screenshots of every official theme option, +as requested by the maintainer. Matching `after-dark--home.png`, `-logs.png`, +`-search.png`, and `-details.png` files are kept in `dev/artifacts/dozzle/559/`. +Keep screenshots out of the production asset tree. + +Not covered: authenticated sessions, linked Cloud features, shell/attach, +container actions, remote agents, Swarm, Kubernetes, URL subfolders, every +community palette, and older Dozzle versions. Notification forms were inspected +without saving destinations, testing webhooks, or sending notifications. +The unlinked `/api/cloud/config` response is 404 in native and themed runs. +That is not a theme regression. + +## Stop + +```sh +docker compose -f dev/dozzle/compose.yaml down +``` + +This removes the task's containers and network, including the log samples. +Add `-v` only to discard the disposable Dozzle profile volume as well. diff --git a/dev/dozzle/compose.yaml b/dev/dozzle/compose.yaml new file mode 100644 index 0000000000..4246c16c82 --- /dev/null +++ b/dev/dozzle/compose.yaml @@ -0,0 +1,61 @@ +name: themepark-dozzle-dev +services: + css: + image: python:3.12-alpine + working_dir: /site + command: ["python", "/serve.py"] + ports: ["127.0.0.1:18865:8000"] + volumes: + - ../../css:/site/css:ro + - ../../resources:/site/resources:ro + - ../serve.py:/serve.py:ro + dozzle: + image: amir20/dozzle:${DOZZLE_TAG:-v11.0.1} + environment: + DOZZLE_FILTER: label=dev.themepark.sample=dozzle + DOZZLE_NO_ANALYTICS: "true" + DOZZLE_ENABLE_ACTIONS: "false" + DOZZLE_ENABLE_SHELL: "false" + ports: ["127.0.0.1:18080:8080"] + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - data:/data + proxy: + image: nginx:stable-alpine + depends_on: [css, dozzle] + environment: + TP_THEME: ${TP_THEME:-dracula} + TP_THEME_FOLDER: ${TP_THEME_FOLDER:-theme-options} + ports: ["127.0.0.1:18081:8080"] + volumes: + - ./default.conf.template:/etc/nginx/templates/default.conf.template:ro + sample-web: + image: python:3.12-alpine + command: ["python", "-u", "/sample-logs.py"] + environment: + SAMPLE_NAME: web + labels: + dev.themepark.sample: dozzle + dev.dozzle.name: sample-web + volumes: ["./sample-logs.py:/sample-logs.py:ro"] + sample-worker: + image: python:3.12-alpine + command: ["python", "-u", "/sample-logs.py"] + environment: + SAMPLE_NAME: worker + labels: + dev.themepark.sample: dozzle + dev.dozzle.name: sample-worker + volumes: ["./sample-logs.py:/sample-logs.py:ro"] + sample-stopped: + image: python:3.12-alpine + command: ["python", "-u", "/sample-logs.py"] + environment: + SAMPLE_NAME: stopped + SAMPLE_EXIT: "true" + labels: + dev.themepark.sample: dozzle + dev.dozzle.name: sample-stopped + volumes: ["./sample-logs.py:/sample-logs.py:ro"] +volumes: + data: diff --git a/dev/dozzle/default.conf.template b/dev/dozzle/default.conf.template new file mode 100644 index 0000000000..44d5ec66f2 --- /dev/null +++ b/dev/dozzle/default.conf.template @@ -0,0 +1,25 @@ +server { + listen 8080; + server_name localhost; + + location / { + proxy_pass http://dozzle:8080; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header Accept-Encoding ""; + proxy_hide_header Content-Security-Policy; + proxy_hide_header X-WebKit-CSP; + sub_filter_once on; + sub_filter '' ''; + } + + location /api { + proxy_pass http://dozzle:8080; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 1h; + } +} diff --git a/dev/dozzle/sample-logs.py b/dev/dozzle/sample-logs.py new file mode 100644 index 0000000000..cbcd4797c6 --- /dev/null +++ b/dev/dozzle/sample-logs.py @@ -0,0 +1,22 @@ +"""Emit synthetic log levels for local Dozzle screenshots without external services.""" +import json +import os +import time +from datetime import datetime, timezone + +name = os.environ.get("SAMPLE_NAME", "sample") +for level in ["debug", "info", "warn", "error"]: + print(json.dumps({"time": datetime.now(timezone.utc).isoformat(), "level": level, + "message": f"Synthetic {name} {level} message for theme inspection", + "context": {"attempt": 2, "ready": True, "result": None}})) +print("Synthetic multiline message\n first continuation\n second continuation") +print("ANSI palette: " + " ".join(f"\033[{color}mColor {color}\033[0m" for color in range(30, 38))) +if os.environ.get("SAMPLE_EXIT") == "true": + raise SystemExit(1) +sequence = 0 +while True: + sequence += 1 + level = ["info", "warn", "error", "debug"][sequence % 4] + print(json.dumps({"time": datetime.now(timezone.utc).isoformat(), "level": level, + "message": f"Synthetic {name} heartbeat", "sequence": sequence})) + time.sleep(5) diff --git a/dev/jellyfin/README.md b/dev/jellyfin/README.md new file mode 100644 index 0000000000..862a789909 --- /dev/null +++ b/dev/jellyfin/README.md @@ -0,0 +1,205 @@ +# Local Jellyfin theme development + +This fixture is for [issue #724](https://github.com/themepark-dev/theme.park/issues/724), +which contains only the title "Jellyfin 12 update?". It does not identify a theme, +browser, injection method, or specific broken screen. + +The target is stable Jellyfin Server and Web 12.0, released September 8, 2026. +The official `jellyfin/jellyfin:12.0` image was tested at digest +`sha256:baba630419915985442f315f08b0cf46d9f4c8a0cc4bd38e94a6d35751dd5ef5`. +The maintainer does not require older-release compatibility for this refresh. + +## Start + +The optional sample generator needs Python, Docker, Node, and Playwright Firefox. +Keep browser dependencies outside the checkout: + +```sh +npm install --prefix /tmp/themepark-browser playwright +/tmp/themepark-browser/node_modules/.bin/playwright install firefox +NODE_PATH=/tmp/themepark-browser/node_modules python3 dev/jellyfin/seed-media.py +docker compose -f dev/jellyfin/compose.yaml up -d +``` + +Wait for `http://localhost:18086/System/Info/Public` to return JSON, then run: + +```sh +python3 dev/jellyfin/setup.py +``` + +The setup script completes the first-run wizard only on an unconfigured instance. +It adds a Sample Movies library if missing and installs Aquamarine only if the +Custom CSS setting is empty. It does not reset an existing profile or theme. +New fixtures use `admin` / `admin`. The existing review instance retains +`themepark` / `themepark-local`; it predates the shared login convention. To rerun +setup there, set `JELLYFIN_DEV_USER=themepark` and +`JELLYFIN_DEV_PASSWORD=themepark-local`. Jellyfin +12 requires a nonempty initial password. Remote access is enabled for Docker's +bridge connection, but only the loopback HTTP port is published. Automatic port +mapping is disabled. + +| Address | Purpose | +| --- | --- | +| http://localhost:18086/web/ | Jellyfin UI. | +| http://localhost:18087/web/ | Nginx injection, including the dashboard. | +| http://localhost:18869 | Local CSS and resources. | + +The generator creates six fictional movies with SVG artwork rendered to PNG, +local NFO metadata, and a short test-pattern video. Movie metadata and image +fetchers are disabled. Media is mounted read-only. Generated files stay in +ignored `dev/artifacts/jellyfin/724/media`. Do not rerun the generator during +maintainer review; it replaces the sample files. + +## Injection + +Use **Dashboard > Branding > Custom CSS code** in Jellyfin 12: + +```css +@import url("http://localhost:18869/css/base/jellyfin/jellyfin-base.css"); +@import url("http://localhost:18869/css/theme-options/aquamarine.css"); +``` + +Edit the source stylesheet and refresh. The local CSS server disables caching. +These imports follow the app's built-in Custom CSS method. Generated theme URLs +are not needed for source development. + +Jellyfin 12 mounts its `CustomCss` component in the modern and legacy client +layouts, but not the admin dashboard layout. Opening `/web/#/dashboard` removes +the injected style element. The direct port therefore keeps the native dashboard +theme. The docs' older +**General > Branding** path also needs updating to the separate Branding page. + +### Subfiltering for the dashboard + +Open http://localhost:18087/web/#/dashboard to include admin pages. The nginx +service follows the [subfiltering guide](https://docs.theme-park.dev/setup/#nginx). +It inserts the base stylesheet and Aquamarine links before ``, once per +HTML response. It requests uncompressed HTML under `/web/`. API, media, and +WebSocket requests use a separate location without injection. No CSP change +was needed. The original port 18086 stays available for built-in CSS comparisons. + +The injected links survive navigation between home and the dashboard. The +fixture retains its existing Branding imports so the direct port stays themed. +This means the client pages load the same theme through both methods on the +proxy. Keep both theme choices aligned; for a deployment using only +subfiltering, omit the duplicate Branding imports. Do not clear a maintainer's +settings during review. Dashboard checks load only the proxy's two links. + +Change the theme-option URL in `nginx.conf` to switch the proxy theme, then run: + +```sh +docker compose -f dev/jellyfin/compose.yaml exec proxy nginx -t +docker compose -f dev/jellyfin/compose.yaml exec proxy nginx -s reload +``` + +The published proxy port and CSS URLs are for local development. They do not +configure a public deployment or a Jellyfin base URL. + +## Styling hooks + +Jellyfin 12 uses `--jf-palette-*` variables in both Material UI and older client +components. Inspect `src/themes/_base/theme.ts` and `_theme.scss` in the matching +web release. Use public `Mui*` component/state classes rather than generated +`css-*` names. Existing legacy rules still serve detail pages and action sheets. + +Keep palette background variables color-only. Apply full theme backgrounds to +page, app bar, drawer, and dialog elements so gradient options work. Popovers +need an opaque dropdown background to cover the content behind them. Dialog +headers and actions use their own theme variables. + +MUI puts channel variables inside `rgba(channel / opacity)`. theme.park's accent +is comma-separated, so copying it directly into a channel breaks selected and +focus backgrounds. The mapping uses relative RGB channels, such as +`from rgb(var(--accent-color)) r g b`. Test the resulting selected/hover colors +in the browser and after minification. This requires a browser with relative +RGB color support. + +Exclude `.MuiButtonBase-root` from the old global anchor override. MUI uses +anchors for navigation and menu items; forcing every anchor to the link color +otherwise overrides their component states. Preserve semantic error/status +palettes when mapping primary and secondary controls. + +The dashboard's Material React Table components calculate background colors +from the JavaScript theme instead of the overridden CSS palette. Devices and +Activity therefore kept native dark table backgrounds even after injection. +Scoped table rules cover the shell, toolbars, rows, cells, and menu lists. The +column menu also sets a native background on its inner list, covering the themed +popover beneath it. Keep sticky +headers and pinned cells opaque so scrolling content does not show through. +Device artwork fallback colors and severity badges remain app-defined. + +Users cards and the permission groups in Profile, Parental Control, and Add +User still use legacy `.visualCardBox` and `.paperList` elements. The native +theme stylesheet loads after nginx's injected links and applies compiled +background colors. Mapping `--jf-palette-surface-overlay` does not affect these +rules. Scoped overrides restore `--card-background` without changing profile +images. Check the user editing tabs as well as the Users listing; they use +different components. + +Users cards, menu-button hover, action sheets, and mobile views were checked +with Aquamarine, Nord, and Hotline in Firefox 155. The same themes cover Profile, +Parental Control, Add User, and narrow Profile views. No permissions were saved. +Check stylesheet order after opening a route, not only on the initial page. + +## Navigation and checks + +- Home: `/web/#/home`. Open User Menu to inspect the new MUI menu. +- Library: use the Sample Movies navigation link. Open Filter, Sort, and View + settings. The library URL includes `topParentId`; do not hard-code sample IDs + in reusable tools. +- Movie: open a card, then More for the legacy action sheet. Delete media opens + a confirmation; cancel it without deleting a sample. +- Display settings: use User Menu > Settings. Direct navigation to + `#/mypreferencesdisplay` requires `?userId=`. Without that + parameter the tested release remained on its loading spinner. +- Close a menu with Escape before navigating programmatically to another route. + An open menu can leave the underlying page hidden from accessible locators. +- Wait for dropdown animations to finish. Intermediate opacity can make a solid + menu look translucent in a screenshot. +- Find visible Play/Resume buttons by accessible name. Detail pages also contain + hidden replay buttons with the title Play. + +Keep the maintainer's selected theme and profile intact. Test other palettes by +substituting the theme-option response in a separate browser context, then +verify the final source through the unchanged built-in injection. Native theme +selection is stored locally under `-appTheme`; change only the test +context's value. Do not reuse a browser device/session for simultaneous playback +checks, especially while the maintainer is testing SyncPlay. + +## Verification scope + +Firefox 155 desktop checks cover all 11 official options and Catppuccin Latte: +home, user menus, display settings, button hover, and selected dropdown items. +Aquamarine, Nord, and Hotline also cover native light mode and 390 by 844 touch +viewports. The mobile checks load production-minified CSS. Source imports and +rendered screenshots were checked as well as computed colors. + +Library controls, movie details, action sheets, and the confirmation dialog +were inspected in Aquamarine. Chromium 152.0.7977.65 in T3 Code also received +a basic Aquamarine login, home, and detail-page check. + +The nginx path was checked in Firefox 155 with Aquamarine, Nord, and Hotline. +Dashboard, General, Branding, Users, Devices, Activity, Plugins, Networking, +and Scheduled Tasks received desktop captures. The dashboard also received +390 by 844 checks. General settings dropdowns and Save hover, plus Activity +row hover and the column menu, were checked in all three themes. The final +table checks used minified CSS and included a narrow viewport. These are +appearance checks, not validation of every admin +action. No server settings were saved. HTTP and API forwarding passed; WebSocket +upgrade forwarding is configured but its handshake has not been verified. The dashboard's absent Custom CSS was +confirmed in the browser and upstream source. Screenshots and results are under ignored +`dev/artifacts/jellyfin/724/`. + +Automated video playback did not start, with or without theme CSS. The video +remained at time zero with no decoded dimensions. The cause is unresolved, so +playback and its on-screen controls are not verified. Also untested: the full Chromium interaction matrix, TV layout, music/books/live TV, older Jellyfin versions, and the rest +of the community palettes. Do not treat the library checks as covering those. + +## Stop + +```sh +docker compose -f dev/jellyfin/compose.yaml down +``` + +Use `down -v` only to discard this fixture's configuration and cache. Remove its +ignored media directory separately only when you intend to regenerate samples. diff --git a/dev/jellyfin/compose.yaml b/dev/jellyfin/compose.yaml new file mode 100644 index 0000000000..19a5c05fdb --- /dev/null +++ b/dev/jellyfin/compose.yaml @@ -0,0 +1,27 @@ +name: themepark-jellyfin-dev +services: + proxy: + image: nginx:stable-alpine + ports: ["127.0.0.1:18087:80"] + depends_on: [jellyfin, css] + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + jellyfin: + image: jellyfin/jellyfin:12.0 + ports: ["127.0.0.1:18086:8096"] + volumes: + - config:/config + - cache:/cache + - ../artifacts/jellyfin/724/media:/media:ro + css: + image: python:3.12-alpine + working_dir: /site + command: ["python", "/serve.py"] + ports: ["127.0.0.1:18869:8000"] + volumes: + - ../../css:/site/css:ro + - ../../resources:/site/resources:ro + - ../serve.py:/serve.py:ro +volumes: + config: + cache: diff --git a/dev/jellyfin/nginx.conf b/dev/jellyfin/nginx.conf new file mode 100644 index 0000000000..60ab9c5e37 --- /dev/null +++ b/dev/jellyfin/nginx.conf @@ -0,0 +1,32 @@ +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 80; + client_max_body_size 100m; + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 3600s; + proxy_buffering off; + + location /web/ { + proxy_pass http://jellyfin:8096; + proxy_set_header Host $http_host; + proxy_set_header Accept-Encoding ""; + sub_filter '' ''; + sub_filter_once on; + add_header Cache-Control "no-store"; + } + + # API, media, and WebSocket responses do not need HTML injection. + location / { + proxy_pass http://jellyfin:8096; + } +} diff --git a/dev/jellyfin/seed-images.cjs b/dev/jellyfin/seed-images.cjs new file mode 100644 index 0000000000..06e29abdd9 --- /dev/null +++ b/dev/jellyfin/seed-images.cjs @@ -0,0 +1,20 @@ +// Render the fixture's SVG artwork into images Jellyfin can scan. +const { firefox } = require('playwright'); +const fs = require('fs'); +const path = require('path'); +(async () => { + const browser = await firefox.launch(); + try { + const page = await browser.newPage(); + const root = path.resolve(process.argv[2]); + for (const directory of fs.readdirSync(root)) { + for (const kind of ['poster', 'backdrop']) { + const source = path.join(root, directory, kind + '.svg'); + await page.goto('file://' + source); + await page.locator('svg').screenshot({ path: source.replace('.svg', '.png') }); + } + } + } finally { + await browser.close(); + } +})().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/dev/jellyfin/seed-media.py b/dev/jellyfin/seed-media.py new file mode 100644 index 0000000000..e0722b91f6 --- /dev/null +++ b/dev/jellyfin/seed-media.py @@ -0,0 +1,21 @@ +from pathlib import Path +import shutil +root=Path(__file__).resolve().parents[1] / 'artifacts/jellyfin/724/media' +root.mkdir(parents=True, exist_ok=True) +if not (root/'sample.mp4').exists(): + import subprocess + subprocess.run(['docker', 'run', '--rm', '--entrypoint', '/usr/lib/jellyfin-ffmpeg/ffmpeg', + '-v', f'{root}:/out', 'jellyfin/jellyfin:12.0', + '-hide_banner', '-loglevel', 'error', '-f', 'lavfi', + '-i', 'testsrc2=size=640x360:rate=24', '-f', 'lavfi', + '-i', 'sine=frequency=440:sample_rate=48000', '-t', '15', + '-c:v', 'libx264', '-preset', 'ultrafast', '-pix_fmt', 'yuv420p', + '-c:a', 'aac', '-y', '/out/sample.mp4'], check=True) +for i,(title,color) in enumerate([('Northern Lights','#186b77'),('The Quiet Orbit','#33386b'),('Glass Coast','#9a5838'),('After the Rain','#356245'),('Signal Blue','#225f95'),('Distant Gardens','#794978')]): + p=root/'Movies'/f'{title} (2026)';p.mkdir(parents=True,exist_ok=True);shutil.copy(root/'sample.mp4',p/(title+'.mp4')) + (p/'movie.nfo').write_text(f'{title}2026Synthetic sample film for checking theme.park styling. No external media or metadata.1AdventureSample StudioPGtrue') + for shape,w,h in [('poster',600,900),('backdrop',1280,720)]: + (p/(shape+'.svg')).write_text(f'''{title}THEME.PARK / SAMPLE {i+1:02}''') + +import subprocess +subprocess.run(["node", str(Path(__file__).with_name("seed-images.cjs")), str(root/"Movies")], check=True) diff --git a/dev/jellyfin/setup.py b/dev/jellyfin/setup.py new file mode 100644 index 0000000000..acf487db6c --- /dev/null +++ b/dev/jellyfin/setup.py @@ -0,0 +1,49 @@ +"""Configure the disposable Compose instance and its synthetic movie library.""" +import json +import os +from urllib.request import Request, urlopen + +USERNAME = os.environ.get('JELLYFIN_DEV_USER', 'admin') +PASSWORD = os.environ.get('JELLYFIN_DEV_PASSWORD', 'admin') + +BASE = 'http://localhost:18086' +AUTHORIZATION = 'MediaBrowser Client="themepark-dev", Device="Local fixture", DeviceId="themepark-jellyfin-dev", Version="1.0"' + + +def request(path, data=None, method=None): + headers = {'Content-Type': 'application/json', 'Authorization': AUTHORIZATION} + payload = json.dumps(data).encode() if data is not None else None + with urlopen(Request(BASE + path, data=payload, headers=headers, method=method)) as response: + content = response.read() + return json.loads(content) if content else None + + +if not request('/System/Info/Public')['StartupWizardCompleted']: + request('/Startup/Configuration', { + 'ServerName': 'theme.park preview', 'UICulture': 'en-US', + 'MetadataCountryCode': 'US', 'PreferredMetadataLanguage': 'en', + }) + request('/Startup/User', {'Name': USERNAME, 'Password': PASSWORD}) + request('/Startup/RemoteAccess', {'EnableRemoteAccess': True, 'EnableAutomaticPortMapping': False}) + request('/Startup/Complete', {}, 'POST') + +session = request('/Users/AuthenticateByName', {'Username': USERNAME, 'Pw': PASSWORD}) +AUTHORIZATION = 'MediaBrowser Token="' + session['AccessToken'] + '"' +if not any(library['Name'] == 'Sample Movies' for library in request('/Library/VirtualFolders')): + request('/Library/VirtualFolders?name=Sample%20Movies&collectionType=movies&refreshLibrary=true', { + 'LibraryOptions': { + 'PathInfos': [{'Path': '/media/Movies'}], 'EnableRealtimeMonitor': False, + 'EnableInternetProviders': False, + 'TypeOptions': [{'Type': 'Movie', 'MetadataFetchers': [], 'ImageFetchers': []}], + }, + }, 'POST') + +branding = request('/System/Configuration/branding') +if not branding.get('CustomCss'): + branding['CustomCss'] = ( + '@import url("http://localhost:18869/css/base/jellyfin/jellyfin-base.css");\n' + '@import url("http://localhost:18869/css/theme-options/aquamarine.css");' + ) + branding['SplashscreenEnabled'] = False + request('/System/Configuration/branding', branding, 'POST') +print('Local fixture ready at ' + BASE + '/web/. User: ' + USERNAME) 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') diff --git a/dev/nzbget/README.md b/dev/nzbget/README.md new file mode 100644 index 0000000000..163def0011 --- /dev/null +++ b/dev/nzbget/README.md @@ -0,0 +1,148 @@ +# Local NZBGet theme development + +Run these commands from the repository root with Docker Compose and Python 3: + +```sh +docker compose -f dev/nzbget/compose.yaml up -d +python3 dev/nzbget/seed.py +``` + +Wait for NZBGet to finish starting before seeding. Open http://localhost:16789 +and log in with username `nzbget` and password `tegbzn6789`. The seed script adds +three paused dummy downloads. It needs no Usenet account and downloads no files. +Running it again adds another three entries. + +The official Docker mod injects HTML stylesheet links pointing at +http://localhost:18765. The CSS server mounts this checkout's `css` and +`resources` directories read-only and sends `Cache-Control: no-store`. Edit +`css/base/nzbget/nzbget-base.css` and refresh the app to see changes. +Compose binds both ports to loopback. The browser resolves `localhost` in +`TP_DOMAIN`. In WSL, open the URL from the same desktop. + +Change the theme or app version by recreating the app container: + +```sh +TP_THEME=dracula docker compose -f dev/nzbget/compose.yaml up -d +TP_THEME=catppuccin-latte TP_COMMUNITY_THEME=true docker compose -f dev/nzbget/compose.yaml up -d +NZBGET_TAG=version-v26.3 docker compose -f dev/nzbget/compose.yaml up -d +``` + +The default version is 25.2, as reported in issue #694. NZBGet's own light and +dark modes are separate from `TP_THEME`. Check both native modes. + +Select one row and check that the header shows a minus. Select all rows and +check that the header shows a checkmark. Then clear the selection. +Also check a download's Files table and the narrow layout. +Capture before/after screenshots using the same theme, selection, and viewport. + +## Modal color checks + +Test Nord, Aquamarine, and Hotline in both native appearance modes. Check Plex +when working on layered page backgrounds. A light option such as Catppuccin +Latte also helps expose text contrast problems. + +| Dialog | How to open it | What to inspect | +| --- | --- | --- | +| Edit download | Click a seeded download's name. | Header, body, footer, inputs, and the statistics table. | +| Files | Open Edit download, then Files. | Scrollable content, row selection, and the footer on a narrow viewport. | +| Add | Click Add above the queue. | The URL input, file controls, and the modal background. Close without submitting. | +| Speed limit | Click the speed display beside the NZBGet logo. | A smaller dialog with the same shared modal styles. Close without saving. | + +Use the native appearance control separately from `TP_THEME`. Version 25.2 has +`#ThemeToggle`. Version 26.3 puts the light/dark choices in the preferences menu. +Inspect `link#ThemeStyleSheet` to confirm which native stylesheet loaded. + +The container's upstream styles are in `/app/nzbget/webui/style.css`, +`dark-theme.css`, `light-theme.css`, and `lib/bootstrap.css`. Inspect the loaded +styles and the theme.park override together. The app may request Bootstrap and +`style.css` through a combined CSS URL. + +### Findings from the local trial + +The native dark stylesheet gives `.modal-body` a `#212529` background. Coloring +`.modal` alone leaves that child panel covering the theme background. The fix +makes the body transparent so the dialog shell supplies its background. + +The footer previously passed `--modal-footer-color` to `background-color`. +That worked with hex colors but failed with a gradient value. A transparent +footer could still appear correct because the dialog behind it used the same +gradient. The header also read the footer variable. Check these sections with +three different temporary header, body-background, and footer values when +verifying their variables. Each section has its own color choice; they do not +need to match. + +The checkbox issue had a different cause. Native dark mode moved the checkmark +and partial-selection icon to positions that do not match theme.park's sprite. +Inspect both `background-image` and `background-position`, including the header +checkbox. A selected row class alone does not prove the checkmark is visible. + +At a 390 by 400 viewport, the Add dialog in 25.2 and 26.3 scrolls with the page. +Its `.modal-body` is as tall as its contents. Scroll the footer into view and +check the document's scroll position rather than assuming the body will scroll. + +Trial screenshots and browser results belong under +`dev/artifacts/nzbget//`. For the modal investigation, +use `modal-colors`. Compare screenshots with the same native mode, theme, and +viewport. Record the app version separately because this setup can run either +25.2 or 26.3. + +## Button hover checks + +Hover Add and the other queue toolbar buttons, then open Add and hover Select +files, Cancel, and Submit. Open Edit download to check its tabs and footer. +In Messages, check the selected All filter as well as unselected filters. +In Settings, check the section controls and Save all changes. Open System to +hover Reload and Shutdown without clicking them. Move the pointer away again +and confirm the resting colors return. Hovering an action button +does not require clicking it or changing the queue or configuration. + +The native dark rule `.btn-default:hover:not(.btn-active)` overrides the +theme's simpler `.btn:hover`. It sets the background to `#212529` and the text +to `inherit`. Match that selector in the shared theme hover rule so default +buttons use `--button-color-hover` and `--button-text-hover` across the app. +Keep the selected-button exclusion. Some Settings controls have their own +transparency or status colors, so do not assert that every button must have +the generic accent color. + +Use actual pointer hover in browser automation, check both native modes, and +include Nord, Aquamarine, and Hotline. Save matching before/after screenshots +under `dev/artifacts/nzbget/button-hover/`. NZBGet can show an update dialog +asynchronously; close it before testing hover so it does not cover the target. +Settings can contain duplicate `Config_Save` IDs, so choose a visible instance +when automating that control. + +### Play/pause icon hover + +The large round status control is separate from `.btn`. Hover the orange +paused icon in `#PlayButton` and the green ready icon in `#PauseButton`. +`#PlayPauseButton` identifies only the orange control. Both use +`.PlayBlockInner` with a child sprite image. + +Upstream hover rules move those images from row `-80px` to `-133px`. The +theme.park sprite has no button images at those hover positions, so the icons +disappear. Keep the green icon at `-113px -80px` and the orange one at +`-177px -80px` on hover. The native `opacity: 0.9` still gives hover feedback. + +Check both states, both native modes, gradient and solid themes, and the narrow +layout. In this disposable setup, first confirm every sample download remains +individually paused. Toggle only the global download pause through JSON-RPC +`pausedownload` and `resumedownload` to expose both icons, then restore the +original state. The main button also changes post-processing and scanning, so +clicking it is not equivalent. Active transfers have a separate animation +layer and need an additional check if that layer is changed. + +Record screenshots under `dev/artifacts/nzbget/play-pause-hover/`. When replacing +an app's sprite sheet, inspect the coordinates for every affected interaction +state. A correct image URL and a visible element do not prove that the selected +rectangle contains any pixels. + +## Stopping the setup + +Stop the setup with: + +```sh +docker compose -f dev/nzbget/compose.yaml down +``` + +To discard this setup's disposable app configuration and queue as well, use +`docker compose -f dev/nzbget/compose.yaml down -v`. diff --git a/dev/nzbget/compose.yaml b/dev/nzbget/compose.yaml new file mode 100644 index 0000000000..f09f0433e1 --- /dev/null +++ b/dev/nzbget/compose.yaml @@ -0,0 +1,33 @@ +name: themepark-nzbget-dev + +services: + css: + image: python:3.12-alpine + working_dir: /site + command: ["python", "/serve.py"] + ports: + - "127.0.0.1:18765:8000" + volumes: + - ../../css:/site/css:ro + - ../../resources:/site/resources:ro + - ../serve.py:/serve.py:ro + + nzbget: + image: lscr.io/linuxserver/nzbget:${NZBGET_TAG:-version-v25.2} + depends_on: + - css + environment: + PUID: "1000" + PGID: "1000" + DOCKER_MODS: ghcr.io/themepark-dev/theme.park:nzbget + TP_DOMAIN: localhost:18765 + TP_SCHEME: http + TP_THEME: ${TP_THEME:-dark} + TP_COMMUNITY_THEME: ${TP_COMMUNITY_THEME:-false} + ports: + - "127.0.0.1:16789:6789" + volumes: + - config:/config + +volumes: + config: diff --git a/dev/nzbget/seed.py b/dev/nzbget/seed.py new file mode 100644 index 0000000000..5c8a178c1c --- /dev/null +++ b/dev/nzbget/seed.py @@ -0,0 +1,45 @@ +"""Add three paused, synthetic queue entries to the local NZBGet test instance.""" + +import base64 +import json +import time +from urllib.request import Request, urlopen + + +def rpc(method, params=None): + # These are the disposable LinuxServer container's default credentials. + credentials = base64.b64encode(b"nzbget:tegbzn6789").decode() + request = Request( + "http://localhost:16789/jsonrpc", + json.dumps({"method": method, "params": params or [], "id": 1}).encode(), + { + "Content-Type": "application/json", + "Authorization": f"Basic {credentials}", + }, + ) + with urlopen(request, timeout=10) as response: + result = json.load(response) + if "error" in result: + raise RuntimeError(result["error"]) + return result["result"] + + +if __name__ == "__main__": + if not rpc("pausedownload"): + raise RuntimeError("Could not pause downloads") + for number in range(1, 4): + nzb = f''' + + + alt.test + checkbox-{number}@example.invalid + +''' + result = rpc("append", [ + f"Checkbox sample {number}.nzb", + base64.b64encode(nzb.encode()).decode(), + "", 0, False, True, "", 0, "ALL", [], + ]) + if result <= 0: + raise RuntimeError(f"Could not add sample {number}: {result}") + print(f"Added paused sample {number} (ID {result})") diff --git a/dev/ombi/README.md b/dev/ombi/README.md new file mode 100644 index 0000000000..8f04dc5585 --- /dev/null +++ b/dev/ombi/README.md @@ -0,0 +1,116 @@ +# Ombi development + +Run from the repository root: + +```sh +docker compose -f dev/ombi/compose.yaml up -d +``` + +Open http://localhost:18085. The default image is +`lscr.io/linuxserver/ombi:version-v4.53.4`, the version reported in issue #715. +The app stores its SQLite databases in the Compose project's `config` volume. +The CSS server uses http://localhost:18868 and mounts this checkout read-only. +Check both ports before starting. + +Complete the wizard with SQLite, skip the media server connection, and create a +local administrator. Choose your own password. No Plex, Sonarr, or Radarr server +is needed to inspect Customization or the Discover genre controls. Discover +loads public media metadata over the network. Its posters and ordering can +change between captures. Do not submit media requests during theme checks. + +## Load local CSS + +Ombi requires its built-in Custom CSS setting. Its +[theme documentation](https://docs.theme-park.dev/themes/ombi/) explicitly rules +out reverse-proxy subfilter injection. + +Open Settings, Configuration, Customization, or navigate directly to +http://localhost:18085/Settings/Customization. Paste this into Custom CSS, +click Submit, and reload: + +```css +@import url("http://localhost:18868/css/base/ombi/ombi-base.css"); +@import url("http://localhost:18868/css/community-theme-options/catppuccin-latte.css"); +``` + +For Nord, Aquamarine, or Hotline, replace the second import with +`http://localhost:18868/css/theme-options/.css`. +Confirm the browser loads both imports plus `css/defaults/placeholders.css` +and `css/defaults/transparent.css` from port 18868. The server sends +`Cache-Control: no-store`, so source edits appear after reloading. +The saved CSS applies to all users, including the login page. Separate browser +contexts still share this setting. + +## Reproduce and check text colors + +On 4.53.4, open `/discover` and inspect the genre buttons, the Combined, Movies, +and TV filters, and the profile name. Open `/Settings/Customization` to inspect +empty field labels, hints, field outlines, and focused or filled inputs. +`/Settings/Ombi` provides another use of the outlined Material fields. Avoid +publishing screenshots of its API key. + +Ombi's component selectors add Angular attributes and set the Discover toggle +groups to white. The theme's group color needs to override that specificity. +The child toggles inherit the group color. Keep the selected filter's foreground +and background paired through `--button-text` and `--button-color`. + +Material sets dark-mode field labels, hints, and outlines to translucent white. +Map outlined field labels and hints to theme text variables. Keep invalid and +disabled fields out of the normal label and outline selectors. The search bar +uses a different field appearance and should retain its existing rules. +Customization's URL inputs did not enter Angular's invalid state when given +malformed URLs, so they are not a useful validation-error test. + +Use real pointer hover and click each filter. Test empty and filled fields, +focus, and a phone viewport with scrolling. Wait for imports, fonts, and color +transitions before screenshots. One early Firefox capture showed labels and +buttons mid-transition even though hints had reached their final colors. + +The #715 checks used Chrome 142 and Firefox 155, with 1495 by 900 and 390 by 844 +viewports. Chrome covered Catppuccin Latte, Nord, Aquamarine, and Hotline. +Firefox covered Latte. The app used its default native dark mode. + +## Version differences and remaining defects + +The 4.53.10 stable release already contains the redesigned navigation and +Discover components covered by the base CSS's section named "v5". Do not infer +the app version from that comment. The new Discover page has no +`.discover-filter-buttons-group` elements. Its Customization page still uses +the outlined Material fields and benefits from the same text fix. + +The #715 patch fixes the reported v4 genre and filter labels, profile name, +and Customization labels, hints, and outlines. Other observed Latte defects +remain separate: + +- In 4.53.4, card type labels use dark text over near-black poster strips. +- The unselected sidebar advanced-search icon is white on a light background. +- In 4.53.10, the hero uses dark text over dark artwork, and some Discover + section headings and genre category labels remain white. +- Some theme accents and search-bar colors still have low contrast in Latte. + +These checks do not establish full light-theme support across Ombi. Keep +run-specific screenshots and results under `dev/artifacts/ombi/`. + +## Change versions or stop + +To try another image: + +```sh +OMBI_TAG=version-v4.53.10 docker compose -f dev/ombi/compose.yaml up -d ombi +``` + +Use disposable configuration for version comparisons. Back up a populated +volume before upgrading, since database migrations may prevent downgrading. +Check the running version with: + +```sh +curl http://localhost:18085/api/v1/Status/info +``` + +Stop the task's services and retain configuration: + +```sh +docker compose -f dev/ombi/compose.yaml down +``` + +To discard this test instance's configuration as well, add `--volumes`. diff --git a/dev/ombi/compose.yaml b/dev/ombi/compose.yaml new file mode 100644 index 0000000000..1839cb484b --- /dev/null +++ b/dev/ombi/compose.yaml @@ -0,0 +1,27 @@ +name: themepark-ombi-dev + +services: + css: + image: python:3.12-alpine + working_dir: /site + command: ["python", "/serve.py"] + ports: + - "127.0.0.1:18868:8000" + volumes: + - ../../css:/site/css:ro + - ../../resources:/site/resources:ro + - ../serve.py:/serve.py:ro + + ombi: + image: lscr.io/linuxserver/ombi:${OMBI_TAG:-version-v4.53.4} + environment: + PUID: "1000" + PGID: "1000" + TZ: Etc/UTC + ports: + - "127.0.0.1:18085:3579" + volumes: + - config:/config + +volumes: + config: diff --git a/dev/pihole/README.md b/dev/pihole/README.md new file mode 100644 index 0000000000..3caa9c5bba --- /dev/null +++ b/dev/pihole/README.md @@ -0,0 +1,108 @@ +# Local Pi-hole theme development + +This setup reproduces [issue #662](https://github.com/themepark-dev/theme.park/issues/662). +The reporter used Pi-hole v5.18.3 and replaced `lcars.css` with Aquamarine. +The fixture runs `pihole/pihole:2026.07.2`, with Core v6.4.3, FTL v6.7, and +Web v6.6. It keeps the native dark theme and uses the +[documented nginx injection method](https://docs.theme-park.dev/themes/pihole/). + +## Start + +Run from the checkout root: + +```sh +docker compose -f dev/pihole/compose.yaml up -d +# Wait until the native web interface loads, then add synthetic devices: +python3 dev/pihole/seed.py +``` + +| Address | Purpose | +| --- | --- | +| http://localhost:18082/admin/network | Native Network page. | +| http://localhost:18083/admin/network | Network page with local theme.park CSS. | +| http://localhost:18866 | Source CSS and resources. | + +The UI has no password and binds to loopback. DNS and DHCP ports are not +published. Configuration lives in a disposable Compose volume. The seed script +writes four synthetic devices into that fixture's database: a recent query, +a query about 12 hours ago, an older query, and a device that never queried. +It uses reserved example addresses and does not send DNS traffic. + +Edit `css/base/pihole/pihole-base.css` and refresh the proxy URL. The server +sends `Cache-Control: no-store`. Change themes with: + +```sh +TP_THEME=nord docker compose -f dev/pihole/compose.yaml up -d proxy +``` + +The default is Aquamarine. Repeat the selected theme override when recreating +the proxy. If the Pi-hole container is recreated, recreate the proxy too so +nginx resolves its new address. + +The proxy requests uncompressed HTML, injects two stylesheet links before +``, and adjusts CSP to allow the local CSS origin. `/api` bypasses HTML +injection. Confirm both source files and their imports return successfully. + +## Network colors are also JavaScript inputs + +Web v6.6's `scripts/js/network.js` reads the computed background colors of +`.network-recent`, `.network-old`, `.network-older`, and `.network-never`. +Its `parseColor()` accepts opaque `rgb(r, g, b)` values only. It interpolates the +recent and old colors to paint rows according to the last query time. + +Replacing the native theme removes these definitions. The computed value becomes +transparent, the parser returns no RGB array, and the row callback throws. +The table stays on Processing even though the API returned devices. An empty +database may hide the failure because no recent-query row reaches that callback. + +Keep these colors opaque. Alpha colors, gradients, and modern color syntax +can break the parser even when the CSS is valid. The base CSS uses Pi-hole's +native dark status colors and restores the matching `.network-gradient` legend. +These indicate device activity, so they do not follow the theme's accent color. + +For a replacement-method reproduction, back up the disposable container's +`/var/www/html/admin/style/themes/default-dark.css`, then replace it with the +base CSS and chosen option. Inline the two default imports, or host them at +working URLs; relative imports otherwise resolve against Pi-hole. Reload the +direct Network URL with seeded devices. Restore the native file afterward and +verify the final CSS through the proxy as well. Never replace files in a real +Pi-hole installation for this test. + +## Verification for #662 + +Firefox 155 checks reproduced the Processing failure and JavaScript exception +when the native dark stylesheet was replaced. The patch loaded all four rows +with all 11 official options using production-minified CSS. Aquamarine, Nord, +and Hotline also passed through nginx injection, including search and successful +loads of both local stylesheets and their imports. A 390 by 844 touch viewport +also loaded all four rows without a JavaScript error. Before/after screenshots and +run results are under ignored `dev/artifacts/pihole/662/`. + +The exact reported v5.18.3 release and LCARS selection were not tested. The +replacement test uses the documented native dark mode on the current release. +This is a Network page fix, not a full Pi-hole theme refresh. Authentication, +other browsers, and community light palettes remain outside this check. + +## Shared borders in Web v6.6 + +The native dark stylesheet adds an opaque gray border to `.box` panels and +separate colors to `.box-header.with-border` and `.table-bordered`. The old +theme only overrode table cells, leaving the outer border and heading divider +in the native palette. Page titles also retain Bootstrap's bright divider. +The theme now uses `--transparency-light-15` for these shared borders. + +The follow-up pass inspected Network, Groups, Dashboard, Query Log, and System +Settings. Use `/admin/settings-system`; `/admin/settings` returns a native 404. +Network was checked with Aquamarine, Nord, and Hotline after the border change. +Status-colored panel headers and device activity colors retain their meaning. +The query log was empty, so populated query rows and their details dialogs were +not covered by this pass. No additional defects were confirmed in the inspected +views. + +## Stop + +```sh +docker compose -f dev/pihole/compose.yaml down +``` + +Add `-v` only to discard this fixture's configuration and synthetic devices. diff --git a/dev/pihole/compose.yaml b/dev/pihole/compose.yaml new file mode 100644 index 0000000000..d67753e211 --- /dev/null +++ b/dev/pihole/compose.yaml @@ -0,0 +1,29 @@ +name: themepark-pihole-dev +services: + pihole: + image: pihole/pihole:2026.07.2 + environment: + FTLCONF_webserver_api_password: "" + FTLCONF_webserver_interface_theme: default-dark + FTLCONF_dns_listeningMode: all + ports: ["127.0.0.1:18082:80"] + volumes: ["data:/etc/pihole"] + css: + image: python:3.12-alpine + working_dir: /site + command: ["python", "/serve.py"] + ports: ["127.0.0.1:18866:8000"] + volumes: + - ../../css:/site/css:ro + - ../../resources:/site/resources:ro + - ../serve.py:/serve.py:ro + proxy: + image: nginx:stable-alpine + depends_on: [pihole, css] + environment: + TP_THEME: ${TP_THEME:-aquamarine} + ports: ["127.0.0.1:18083:8080"] + volumes: + - ./default.conf.template:/etc/nginx/templates/default.conf.template:ro +volumes: + data: diff --git a/dev/pihole/default.conf.template b/dev/pihole/default.conf.template new file mode 100644 index 0000000000..c20153cefc --- /dev/null +++ b/dev/pihole/default.conf.template @@ -0,0 +1,17 @@ +server { + listen 8080; + server_name localhost; + location / { + proxy_pass http://pihole:80; + proxy_set_header Host $http_host; + proxy_set_header Accept-Encoding ""; + proxy_hide_header Content-Security-Policy; + add_header Content-Security-Policy "default-src 'none'; base-uri 'none'; form-action 'self'; frame-src 'self'; font-src 'self'; connect-src 'self'; img-src 'self' data:; manifest-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' http://localhost:18866 'unsafe-inline'"; + sub_filter_once on; + sub_filter '' ''; + } + location /api { + proxy_pass http://pihole:80; + proxy_set_header Host $http_host; + } +} diff --git a/dev/pihole/seed.py b/dev/pihole/seed.py new file mode 100644 index 0000000000..8dddf8cfcf --- /dev/null +++ b/dev/pihole/seed.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Add four synthetic network devices to the disposable Pi-hole database.""" +import subprocess +import time +from pathlib import Path + +compose = Path(__file__).with_name('compose.yaml') +now = int(time.time()) +statements = [] +for index, (name, age) in enumerate( + [('recent', 60), ('today', 43200), ('older', 172800), ('never', 0)], 100 +): + last_query = now - age if age else 0 + statements.append( + f"INSERT OR REPLACE INTO network VALUES ({index}," + f"'02:00:00:00:00:{index:02x}','eth0',{now-604800},{last_query},42," + "'Synthetic device',NULL);" + ) + statements.append( + f"INSERT OR REPLACE INTO network_addresses VALUES ({index}," + f"'192.0.2.{index}',{now},'sample-{name}',{now});" + ) +subprocess.run( + ['docker', 'compose', '-f', str(compose), 'exec', '-T', 'pihole', + 'pihole-FTL', 'sqlite3', '/etc/pihole/pihole-FTL.db', ''.join(statements)], + check=True, +) diff --git a/dev/serve.py b/dev/serve.py new file mode 100644 index 0000000000..8124181fb9 --- /dev/null +++ b/dev/serve.py @@ -0,0 +1,13 @@ +"""Serve the mounted theme.park assets without browser caching during development.""" + +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer + + +class Handler(SimpleHTTPRequestHandler): + def end_headers(self): + self.send_header("Cache-Control", "no-store") + super().end_headers() + + +if __name__ == "__main__": + ThreadingHTTPServer(("0.0.0.0", 8000), Handler).serve_forever() diff --git a/dev/whisparr/.gitignore b/dev/whisparr/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/dev/whisparr/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/dev/whisparr/README.md b/dev/whisparr/README.md new file mode 100644 index 0000000000..8b25b31b0a --- /dev/null +++ b/dev/whisparr/README.md @@ -0,0 +1,69 @@ +# Whisparr development + +Run these commands from the theme.park checkout root. Docker Compose hosts +local CSS at http://127.0.0.1:18875 and starts disposable Hotio instances at +http://127.0.0.1:16969 for v2 and http://127.0.0.1:16970 for v3. +The pinned images contain Whisparr 2.2.0.231 and 3.5.0.1585. + +```sh +TP_ADDON=whisparr-4k-logo docker compose -f dev/whisparr/compose.yaml up -d +``` + +On first launch, choose Forms authentication and create a local test account. +The logo checks work with an empty library. Do not add real media or indexers. +Configuration lives in separate Docker volumes. + +The setup mounts the repository's Whisparr startup script into +`/etc/cont-init.d/98-themepark`, following the +[Hotio setup](https://docs.theme-park.dev/setup/#hotio-containers-s6-overlay-v3-images). +`TP_HOTIO=true` selects `/app/bin/UI`. The script injects the app base CSS, +theme option, and addon before `` in both the app and login pages. +It does not use `DOCKER_MODS`. + +To change themes, recreate both application containers. Restarting alone leaves +the old stylesheet links in their HTML because the script avoids duplicate injection. + +```sh +TP_THEME=aquamarine TP_ADDON=whisparr-4k-logo \ + docker compose -f dev/whisparr/compose.yaml up -d --force-recreate v2 v3 +``` + +Repeat with `TP_THEME=hotline` and `TP_THEME=nord`. Omit `TP_ADDON` when recreating +to capture the original logo. CSS and SVG edits appear on browser reload. +The CSS host sends `Cache-Control: no-store`. + +## Logo checks + +Check the desktop header, mobile header, open mobile menu, loading screen, and +Forms login page. Resize to 1280, 768, 752, 390, and 320 pixels wide. Check hover, +keyboard focus, home navigation, and opening and closing the menu. + +Whisparr v2 uses `PageHeader-logo-` at every width. V3 uses +`PageHeader-logoFull-` on desktop, `PageHeader-logo-` on mobile, and a separate +`PageSidebar-logo-` in the open mobile menu. V2 does not have that sidebar image. +Both versions use `LoadingPage-logoFull-` and `.panel-header > img.logo`. + +To inspect loading, delay the app's API requests in browser automation while +reloading an authenticated session. Release the requests after capturing the +real loading screen. Check asset requests for the addon SVG, base CSS, imported +Radarr and Servarr styles, defaults, and selected theme option. + +Save screenshots and run details under ignored `dev/artifacts/whisparr/550/`. +Issue #550 reported Hotio versions 2.0.0.355 and 3.0.0.530. The pinned test images +are newer, so results do not establish compatibility with those exact builds. + +## Artwork + +`whisparr-4k.svg` uses the purple Whisparr SVG shipped in the pinned v3 image. +Its gold 4K image is the unchanged `Layer 3` inside the `sonarr-4k` group in +`css/addons/sonarr/sonarr-logo.psd`. The SVG embeds that layer so it needs no +external image request. Its square layout matches the Sonarr and Radarr addons. +Keep the upstream W paths and the existing gold lettering when changing placement. + +## Stop and reset + +```sh +docker compose -f dev/whisparr/compose.yaml down +``` + +To discard only these test configurations, add `--volumes` to that command. diff --git a/dev/whisparr/compose.yaml b/dev/whisparr/compose.yaml new file mode 100644 index 0000000000..433eaaa00b --- /dev/null +++ b/dev/whisparr/compose.yaml @@ -0,0 +1,46 @@ +name: themepark-whisparr-dev + +x-whisparr: &whisparr + environment: + PUID: "1000" + PGID: "1000" + TZ: Etc/UTC + TP_HOTIO: "true" + TP_DOMAIN: 127.0.0.1:18875 + TP_SCHEME: http + TP_THEME: ${TP_THEME:-nord} + TP_ADDON: ${TP_ADDON:-} + depends_on: + - css + +services: + css: + image: python:3.12-alpine + working_dir: /site + command: [python, /serve.py] + ports: + - "127.0.0.1:18875:8000" + volumes: + - ../../css:/site/css:ro + - ../../resources:/site/resources:ro + - ../serve.py:/serve.py:ro + v2: + <<: *whisparr + image: ghcr.io/hotio/whisparr:latest@sha256:01d52209b9e24fd8145b2e794f13d75e20cc60254bb57e2a98e0d2dc4a16544d + ports: + - "127.0.0.1:16969:6969" + volumes: + - v2-config:/config + - ../../docker-mods/whisparr/root/etc/cont-init.d/98-themepark:/etc/cont-init.d/98-themepark:ro + v3: + <<: *whisparr + image: ghcr.io/hotio/whisparr:v3@sha256:b18ba2049123f1480a557b23954f1055b86f2625436f0bc51ccaa012030d9ba3 + ports: + - "127.0.0.1:16970:6969" + volumes: + - v3-config:/config + - ../../docker-mods/whisparr/root/etc/cont-init.d/98-themepark:/etc/cont-init.d/98-themepark:ro + +volumes: + v2-config: + v3-config: