7 docs in /testing/ — institutional memory after 6+ regressions in
24-48h on the v6 theme. Read before any edit.
README.md — index + quickstart
THEMING.md — safe-edit checklist + layer/specificity tables
ERROR-PATTERNS.md — 12 cataloged patterns (Symptom/Cause/Diag/Fix/Prev)
HEADLESS-PROBE.md — 11 playwright recipes (md5 chain, darkPct,
ancestor bg sample, dropdown listItem probe)
ROLLBACK.md — 8 emergency revert recipes (overlay, branding,
encoding, full-from-repo, dev-clone-prod,
git-revert, pw-reset, bind-mount inode-swap)
SMOKE-TEST.md — manual + headless verify checklist
DEPLOY.md — dev → prod promotion workflow with backup +
chown root + restart inode-swap
Empty subdirs: snipUSER-Es/, recipes/, incidents/ (post-mortems land here).
Goal: stop reinventing the same fixes. Catalog every error class,
codify the recovery, build a skills folder for future ARRFLIX work.
123 lines
7.6 KiB
Markdown
123 lines
7.6 KiB
Markdown
# THEMING — how to edit the ARRFLIX theme without breaking it
|
||
|
||
> Short, actionable companion to `docs/31-theme-layer-model-and-edit-guide.md`.
|
||
> Read 31 once for the why; come back here for the checklist every edit.
|
||
|
||
## TL;DR — checklist before EVERY theme edit
|
||
|
||
1. Read `docs/31-theme-layer-model-and-edit-guide.md` (canonical layer model).
|
||
2. Decide: **am I painting an ancestor of `<video>`?** (see layer table below).
|
||
3. If yes → scope with `body.arrflix-themed:not(.arrflix-video-active)` AND add the matching transparent rule under `body.arrflix-themed.arrflix-video-active`.
|
||
4. Use the specificity table to predict what wins. `!important` does NOT promote specificity.
|
||
5. Edit `bin/inject-middle-theme.py` — NEVER hot-patch the deployed overlay.
|
||
6. `python3 bin/inject-middle-theme.py` → `scp web-overrides/index.html dev:/opt/docker/jellyfin-dev/web-overrides/` → `docker restart jellyfin-dev`.
|
||
7. Hard-refresh browser (`Ctrl+Shift+R`) — bind-mount serves stale otherwise.
|
||
8. Run `testing/SMOKE-TEST.md` (login, home, video, OSD).
|
||
9. Green? Promote per `testing/DEPLOY.md`. Red? `testing/ROLLBACK.md`.
|
||
|
||
## The layer model (condensed)
|
||
|
||
| Layer | Element | bg ownership |
|
||
|------:|---------|--------------|
|
||
| 0 | `<html>` | `#000` (JS inline-style pinned via `setProperty(...,'important')`) |
|
||
| 1 | `<body>` | L1 `#000` off-video / L2 `transparent` on-video |
|
||
| 2 | `.backgroundContainer` / `.skinBody` / `#reactRoot` | follows L1/L2 |
|
||
| 3 | `.mainAnimatedPages` / `.pageContainer` | follows L1/L2 |
|
||
| 4 | `.skinHeader` | `#000` off-video, `display:none` on-video |
|
||
| 5 | `.videoPlayerContainer` (z:1000) → `<video.htmlvideoplayer>` | transparent on-video |
|
||
| 6 | `.osdControls` / `.videoOsdBottom` (z:~1100–1500) | DO NOT touch — Jellyfin owns |
|
||
| 7 | `.dialogContainer` / `.actionSheet` (z:~2000+) | DO NOT touch — Jellyfin owns |
|
||
|
||
## Specificity quick reference
|
||
|
||
| Selector | (a,b,c) | When to use |
|
||
|----------|---------|-------------|
|
||
| `body` | (0,0,1) | almost never |
|
||
| `body.arrflix-themed` | (0,1,1) | base theme rule, off/on video both |
|
||
| `body.arrflix-themed:not(.arrflix-video-active)` | (0,2,1) | **L1**: off-video bg paint |
|
||
| `body.arrflix-themed.arrflix-video-active` | (0,2,1) | **L2**: on-video transparent |
|
||
| `body.arrflix-themed.arrflix-video-active #videoOsdPage` | (1,2,1) | beats Cineplex `#videoOsdPage .pageContainer` (1,1,0) |
|
||
| `body.arrflix-video-active:not(:has(#loginPage:not(.hide))) .skinHeader` | (0,4,2) | beats Cineplex `display:flex` on header |
|
||
|
||
L1 and L2 tie on (0,2,1). **Source order decides** — L2 must come AFTER L1 in `inject-middle-theme.py`. Reordering reopens the black-screen bug.
|
||
|
||
## DO NOT DO
|
||
|
||
- Set `z-index` on `<video>` or `.videoPlayerContainer` above 1000 → covers OSD scrubber/buttons (image-12 incident).
|
||
- Add `background-color` rules without `:not(.arrflix-video-active)` gate → black-screen-over-video.
|
||
- Hot-patch `/opt/docker/jellyfin/web-overrides/index.html` in place → repo↔prod drift, INC1 root cause.
|
||
- `cp` overlay then skip `docker restart jellyfin` → bind-mount inode swap, container serves stale.
|
||
- Use `:has(.htmlVideoPlayer)` (camelCase) — class is `.htmlvideoplayer` lowercase. The selector silently never matches.
|
||
- Drop a `<video>` literal into `branding.xml` `<CustomCss>` (even in a comment) without escaping → XML parse fails silently, theme disappears site-wide. Use `<video>`.
|
||
- Add `!important` hoping it beats a higher-specificity rule. Among `!important` rules, specificity still wins.
|
||
|
||
## When to add to L1/L2 paired rules
|
||
|
||
If your rule paints `background`, `background-color`, or `background-image` on **any** of:
|
||
|
||
```
|
||
body, html, .backgroundContainer, .skinBody, .mainAnimatedPage, .mainAnimatedPages,
|
||
.pageContainer, #reactRoot, .videoPlayerContainer, #videoOsdPage, .libraryPage,
|
||
video.htmlvideoplayer, .emby-scroller, .backdropContainer
|
||
```
|
||
|
||
→ Add the selector to **BOTH** lists in `bin/inject-middle-theme.py`:
|
||
|
||
- **L1 list** — under `/* --- L1: PURE-BLACK BG (off-video only) ------ */`, prefixed with `body.arrflix-themed:not(.arrflix-video-active)`.
|
||
- **L2 list** — under the L2 transparent block, prefixed with `body.arrflix-themed.arrflix-video-active`, value `background:transparent !important`.
|
||
|
||
Always paired. Off-video must stay opaque black; on-video must be transparent so `<video>` pixels show through.
|
||
|
||
## Safe-edit recipe — "make the search input focus ring red"
|
||
|
||
```bash
|
||
# 1. Edit the injector (NOT the deployed overlay)
|
||
$EDITOR /tmp/arrflix-recon/bin/inject-middle-theme.py
|
||
# Add inside the CSS string, search-input section:
|
||
# body.arrflix-themed .searchFields input:focus {
|
||
# border-color: #E50914 !important;
|
||
# box-shadow: 0 0 0 2px rgba(229,9,20,.35) !important;
|
||
# }
|
||
# Specificity (0,2,1) — does NOT touch a <video> ancestor → no L1/L2 pairing needed.
|
||
|
||
# 2. Regenerate the overlay
|
||
cd /tmp/arrflix-recon && python3 bin/inject-middle-theme.py
|
||
|
||
# 3. Sanity: exactly one marker block
|
||
grep -c ARRFLIX-MIDDLE-THEME-BEGIN web-overrides/index.html # = 1
|
||
|
||
# 4. Push to dev only
|
||
scp web-overrides/index.html nullstone:/opt/docker/jellyfin-dev/web-overrides/index.html
|
||
ssh nullstone 'docker restart jellyfin-dev'
|
||
|
||
# 5. Verify served
|
||
curl -s https://dev.arrflix.s8n.ru/web/index.html | grep -c ARRFLIX-MIDDLE-THEME-BEGIN # = 1
|
||
|
||
# 6. Hard-refresh browser, run testing/SMOKE-TEST.md, then promote per testing/DEPLOY.md.
|
||
```
|
||
|
||
## How to add a new skin variant
|
||
|
||
Skin variants are alternative CSS blocks that swap a single visual concern (selector highlight, header logo treatment, etc.) without forking the whole theme.
|
||
|
||
- Location: `web-overrides/skins/`.
|
||
- Naming: `<concern>-variant-<NN>-<short-slug>.css` (e.g. `selector-variant-02-red-underline.css`).
|
||
- Format: file-level comment header explaining what concern it replaces, which variant is currently active in `inject-middle-theme.py`, and a "drop into the CSS string" instruction. Body is plain CSS scoped under `body.arrflix-themed …`.
|
||
- Activation: copy the rules into the matching section of `bin/inject-middle-theme.py`, regen overlay, deploy. Skins are NOT auto-loaded — the file is a parking spot.
|
||
|
||
## Common pitfalls
|
||
|
||
- **camelCase vs lowercase classes** — `.htmlVideoPlayer` does NOT exist; the real class is `.htmlvideoplayer`. Same trap on `.videoOsdBottom` (correct) vs `.videoosdbottom` (wrong).
|
||
- **Cineplex CSS load order** — `branding.xml` → `@import url('/web/cineplex.css')` is injected as a `<style>` AFTER our inline block. On equal specificity Cineplex wins. Bump specificity, do NOT reorder.
|
||
- **`branding.xml` XML parse** — `<CustomCss>` content must be XML-safe. Escape `<` `>` in any CSS comment that mentions HTML tags. Silent failure = whole branding skipped.
|
||
- **iframes / shadow DOM** — Jellyfin web does not currently use either. N/A; skip.
|
||
- **`backdrop-filter: blur()`** — only renders if there's content scrolling/painted behind the fixed element. On a pure-black bg the blur is invisible (no pixel diff). Test on a page with a poster backdrop.
|
||
- **`getComputedStyle(html).backgroundColor` returns `rgba(0,0,0,0)`** despite stylesheet rules — Chromium root-canvas quirk. We pin `<html>` via JS `style.setProperty('background-color','#000','important')`. Don't fight it from CSS.
|
||
|
||
## See also
|
||
|
||
- `docs/31-theme-layer-model-and-edit-guide.md` — canonical layer model and history of past incidents.
|
||
- `testing/ERROR-PATTERNS.md` — catalog of past mistakes (INC1–INC7, v6-stable, image-12).
|
||
- `testing/SMOKE-TEST.md` — 4-step manual verify after any theme change.
|
||
- `testing/HEADLESS-PROBE.md` — Playwright recipes for DOM / `darkPct` / OSD-visible assertions.
|
||
- `testing/DEPLOY.md` / `testing/ROLLBACK.md` — promote-to-prod and revert procedures.
|