diff --git a/CHANGELOG.md b/CHANGELOG.md
index a6555da9..0e21f636 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,46 @@ GitHub. The full reasoning and the measurements behind each change are in [ROADM
## Unreleased
+### Control panel
+
+- **Interface themes [770].** Settings → Application → Interface Theme offers seven themes and
+ Device default:
+ - CaYaDev, the existing look and still the default;
+ - Dark, a neutral graphite;
+ - Light;
+ - High Contrast, black with yellow, held to WCAG AAA (7:1 for text);
+ - Midnight, Sepia and Neon.
+
+ Device default follows the operating system's light or dark preference and switches live.
+ The theme changes the panel only. The visualizer output, the stream overlay and your own
+ colours stay as they are. It is saved in the settings file, so a backup carries it, and
+ "Reset to Defaults" keeps it.
+- **Every panel colour comes from one table.** `src/shared/ui-themes.js` defines 64 colour tokens
+ per theme. `admin.css` and `studio.css` no longer contain a colour outside the CaYaDev `:root`
+ block. The timeline and projection-mapping canvases read the same tokens and redraw when the
+ theme changes. The window opens in the theme's background, so a light theme does not flash
+ dark first.
+- **Readability is tested, not assumed.**
+ - Every theme is checked against 128 contrast rules: 4.5:1 for text on every surface, button
+ text in the middle of the gradient, the code editors, focus rings and switch knobs at 3:1.
+ - Every rule in the panel's CSS that sets both a text colour and a background is measured in
+ every theme.
+ - A guard fails when a colour is written into the panel CSS or a panel script outside the
+ table, so a new panel cannot break a theme quietly.
+ - The self-test selects every theme from the picker, walks every category, and measures the
+ colour Chromium actually draws for each visible text against its background: about 2,600
+ texts per theme, none below the threshold.
+- **Three CaYaDev colours moved so the default theme passes the same test.**
+ - The small hint text was 3.4–4.4:1 on cards and is now at least 4.5:1.
+ - Code comments in the shader editor were 3.3:1.
+ - White text on the solid red danger button was 3.9:1.
+ - The end of the red gradient on filled buttons is slightly deeper, so white text reads in the
+ middle of the gradient too.
+- **Two labels were unreadable in every theme.** The empty side of the A/B crossfader and the
+ "built-in" tag in Studio's list were dimmed with opacity to 2.9:1 and 3.7:1. They now use the
+ secondary text colour. The decorative "·" separators in status lines are hidden from screen
+ readers.
+
### Video export
- **Eleven formats instead of one MP4 [521].** H.264 and H.265 in MP4, ProRes 422 HQ and DNxHR HQ
diff --git a/README.md b/README.md
index 43e53ff1..e86c6683 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ layers and **40 GPU effects**, an **OBS overlay**, **Spout / Syphon**, **project
[](https://github.com/CaYatur/SoundVisualizer/releases)
[](LICENSE)
[](#download)
-[](#tests)
+[](#tests)
[](https://www.electronjs.org/)
@@ -1154,6 +1154,9 @@ return the capture diagnosis and the live analysis (key, chord, pitch, loudness,
slider limits 5×.
- **Turkish and English**, switchable at runtime; the self-test fails if any interface string is
left untranslated.
+- 3.1.6 **Interface themes**: CaYaDev, Dark, Light, High Contrast, Midnight, Sepia and
+ Neon, or Device default to follow your system's light or dark setting. Every theme is
+ contrast-tested: the self-test measures every text it draws.
---
@@ -1741,7 +1744,7 @@ npm test
npm start -- --smoke
```
-**3189 unit tests, all passing.** They are written to check answers, not to exercise lines:
+**3222 unit tests, all passing.** They are written to check answers, not to exercise lines:
- **Formulas** are checked against values derived by hand from their definitions — Viviani's curve
staying on its sphere, the torus tube radius, Chladni's m↔n antisymmetry, every attractor staying
diff --git a/README.tr.md b/README.tr.md
index 945fb0bf..bb136632 100644
--- a/README.tr.md
+++ b/README.tr.md
@@ -18,7 +18,7 @@ destesi**, ve **MCP üzerinden yapay zekâ kontrolü**.
[](https://github.com/CaYatur/SoundVisualizer/releases)
[](LICENSE)
[](#hızlı-başlangıç)
-[](#testler)
+[](#testler)
[](https://www.electronjs.org/)
@@ -1159,6 +1159,9 @@ tanısını ve canlı çözümlemeyi (ton, akor, perde, gürlük, davul bantlar
aralıklar** sürgü sınırlarını 5 katına çıkarıyor.
- Çalışırken değiştirilebilen **Türkçe ve İngilizce**; çevrilmemiş tek bir arayüz metni kalırsa öz test
başarısız oluyor.
+- 3.1.6 **Arayüz temaları**: CaYaDev, Karanlık, Aydınlık, Yüksek Karşıtlık, Gece Mavisi, Sepya
+ ve Neon; ya da sistemin açık/koyu ayarını izleyen Cihaz varsayılanı. Her temanın karşıtlığı
+ sınanıyor: öz test çizdiği her yazıyı ölçüyor.
---
@@ -1744,7 +1747,7 @@ npm test
npm start -- --smoke
```
-**3189 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar:
+**3222 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar:
- **Formüller**, tanımlarından elle türetilmiş değerlerle sınanıyor — Viviani eğrisinin küre üzerinde
kalması, simidin boru yarıçapı, Chladni'nin m↔n antisimetrisi, her çekicinin sınırlı kalması ve görüş
diff --git a/ROADMAP.md b/ROADMAP.md
index cf019cc9..da29d21e 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -93,6 +93,7 @@ hands, and a row only moves up once it ships.
|---|:--:|---|
| Per-app audio capture on macOS and Linux | v3.1.6 | On `main` (#514, #517). Linux verified in WSLg with PulseAudio; macOS written on ScreenCaptureKit and compiled in CI, never run on a Mac. Windows shipped in v3.1.3 |
| Live level meter per application | v3.1.6 | Done on `main` (#516): a moving dB-scale bar per application, streamed only while the picker is on screen |
+| Interface themes | v3.1.6 | Done on `main` (#770): CaYaDev, Dark, Light, Device default, High Contrast, Midnight, Sepia and Neon; contrast tested in every theme |
| Comprehensive video export | v3.1.6 | Done on `main`: formats, alpha and frame sequences (#521, #525, #529), about 3× faster export (#531), duration sources, metadata and clean cancel (#523, #524, #528, #532), profiles (#522), chapters (#527), batch queue (#526), the two render modes (#530) |
| Broadcast layout editor | v3.1.7 | Not started |
| NDI | v3.1.8 | Deferred for licence reasons, not difficulty — see "Not done, and why" |
@@ -131,7 +132,7 @@ npm test
npm start -- --smoke
```
-- **3189 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0;
+- **3222 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0;
105 came with v3.1.1; 163 came with v3.1.2; 157 came with v3.1.3 — 1128 at
that tag — 469 more with v3.1.4, most of them from the MilkDrop work — 1597 at
that tag — 1402 more with v3.1.5 — 2999 at that tag — and 27
@@ -3567,6 +3568,47 @@ Export is about three times faster for MP4 and WebM, with no change to the pictu
Nothing is worse by more than 1.03 except "fed - DSC-011", which went from black to MilkDrop's coloured fields.
- **The harness runs in parallel** (`--jobs=N`). MilkDrop instances and engine processes share the list. On 100 presets, MilkDrop's 60 frames took 113 s on 8 jobs, against about 98 s for 13 presets before. Results match the sequential run for every noise-free preset.
+### Interface themes (#770) · done
+
+The panel had one look. It now has seven themes and Device default, chosen under Settings → Application → Interface Theme.
+
+- **One table.** `src/shared/ui-themes.js` holds 64 colour tokens per theme: surfaces, text, accent, states, overlays, the clip deck and performance view, the code editors and the media backdrop. Some tokens are channels (`--fg-rgb: 255 255 255`) that the CSS uses with an alpha, as `rgb(var(--fg-rgb) / .05)`. A white hover tint on a dark theme becomes a dark tint on a light theme, and the alpha stays in the CSS.
+- **Nothing else holds a colour.**
+ - About 320 colour literals in `admin.css` and `studio.css` became tokens.
+ - Undefined variables with fallbacks (`--fg`, `--bad`, `--err`) now point at real tokens.
+ - The timeline and projection-mapping canvases read tokens at draw time and redraw on a theme change.
+ - Colours left in panel scripts are user content: palette defaults, clip colours, the recording's own frame. They are listed with a reason.
+- **What stays fixed.**
+ - The media preview backdrop is dark in every theme, because a picture sits on it.
+ - The visualizer output, the stream overlay and user colours are not themed.
+ - `nativeTheme.themeSource` is never set, because it would change `prefers-color-scheme` in the visualizer window too.
+- **No dark flash.**
+ - The main process opens the panel window in the theme's background colour and passes the theme in the page address.
+ - A script in `
` applies it before the body paints.
+ - The address is updated on change, so the language switch's reload opens in the same theme.
+- **Device default** resolves to Dark or Light from `prefers-color-scheme` and follows a change live, measured with an emulated scheme flip.
+- **Contrast thresholds** come from WCAG 2.2. Text is 4.5:1, since most panel text is 10.5–13 px. Focus rings, meaningful borders and switch knobs are 3:1. High Contrast is held to 7:1.
+ - `--accent2` is both heading text and the end of the button gradient. It cannot be both: a red that reads at 4.5:1 on a dark card cannot carry white text at 4.5:1. So the gradient end became its own token, `--accent-grad`.
+- **Measured before and after in CaYaDev.**
+ - Panel screenshots of every category in both languages, taken before any CSS change, were compared pixel by pixel.
+ - With the four intentional changes set back to their old values, the remaining differences were live content (BPM, the app list, meters), the new picker, and one consolidated green on the lighting status.
+- **Tests that fail on misuse.**
+ - Every theme defines exactly the token set.
+ - `:root` equals the CaYaDev table.
+ - No colour literal outside `:root`.
+ - Every `var()` is defined, and channel tokens are used only as channels.
+ - Panel scripts carry no colour outside the content list, and the list cannot go stale.
+ - 128 contrast rules per theme.
+ - Every CSS rule with both a text colour and a background is measured in every theme. This caught the "Live" pill in Light and Sepia at 4.17:1.
+ - The head script was tested in a fake DOM.
+- **Self-test.** Every theme is picked from the panel's own picker, every category is scrolled through, and the colour Chromium draws for each visible text is measured against its composited background.
+ - About 2,600 texts per theme, none below the threshold.
+ - The lowest ratio per theme: CaYaDev 4.54, Dark 4.54, Light 4.96, High Contrast 7.8, Midnight 4.54, Sepia 5.12, Neon 4.54.
+ - The first run found the decorative status separators and a dimmed link.
+ - Two measurement traps:
+ - A gradient's `transparent` stop was averaged as black.
+ - In a background window, CSS transitions do not advance, so the old theme's colour was measured. Pending transitions are now finished before measuring.
+
## v3.1.7 — Broadcast layout editor
v3.0.0 already ships the broadcast template group: a restrained bar
diff --git a/src/admin/admin.css b/src/admin/admin.css
index f2170eab..2e006b91 100644
--- a/src/admin/admin.css
+++ b/src/admin/admin.css
@@ -5,30 +5,91 @@
* SPDX-License-Identifier: MIT
*/
:root {
+ /* Arayüz teması (#770). Bu blok CaYaDev temasının AYNISI: renkler
+ src/shared/ui-themes.js tablosundan gelir ve test ikisini karşılaştırır.
+ Başka bir tema seçilince theme-boot.js değerleri e yazar.
+ Bu dosyada :root dışında renk yazılmaz; bkz. tests/ui-themes.test.js. */
+ /* Yüzeyler */
--bg: #0b0910;
--bg2: #120d13;
--surface: #17111a;
--card: #1b141f;
--card2: #241a29;
--card3: #2c2032;
+ --card-end: rgba(16, 11, 18, .9);
+ --popover: rgba(20, 14, 22, .985);
+ --scrim: rgba(3, 3, 8, .62);
+ --glow-a: #3d1016;
+ --glow-b: #1d1030;
--line: #34222f;
--line2: #472c3a;
+ /* Yazı */
--text: #f4edf1;
+ --text-strong: #ffffff;
--muted: #b2969f;
- --muted2: #8a6f79;
+ --muted2: #a1858f;
+ --brand-ink: #ffffff;
+ /* Vurgu */
--accent: #e11d2a;
--accent2: #ff4d5e;
- --accent-soft: rgba(225, 29, 42, .14);
+ --accent-grad: #ea2636;
+ --accent-rgb: 225 29 42;
+ --on-accent: #ffffff;
+ --on-solid: #ffffff;
+ --accent-text: #ff9aa3;
+ --accent-label: #ffc7cd;
+ /* Durumlar */
--ok: #43d98a;
+ --ok-rgb: 67 217 138;
+ --ok-text: #9df0c4;
--warn: #ffc44d;
+ --warn-rgb: 255 196 77;
+ --warn-text: #f3cf82;
+ --on-warn: #1b141f;
+ --danger: #d93a40;
+ --danger-rgb: 255 80 80;
+ --danger-text: #ff8a8a;
+ --solo: #f5a524;
+ --on-solo: #1a1a1a;
+ --lock: #6e56cf;
--off: #6a4a52;
+ --sel-rgb: 124 92 255;
+ --info-rgb: 120 180 255;
+ --tempo: #50d2e6;
+ /* Örtüler ve gölge */
+ --fg-rgb: 255 255 255;
+ --shade-rgb: 0 0 0;
+ --shadow-rgb: 0 0 0;
+ --knob: #ffffff;
+ /* Klip destesi, performans, ölçerler */
+ --cue-live: #2ea043;
+ --cue-armed: #f0b429;
+ --slot-default: #4f8cff;
+ --meter-mid: #ffd54a;
+ --meter-hot: #ff5e6c;
+ /* Kod düzenleyicileri */
+ --code-bg: #0d0a11;
+ --code-gutter: #0a0810;
+ --code-fg: #e7dfe9;
+ --syn-kw: #ff7b8a;
+ --syn-type: #7fd6ff;
+ --syn-fn: #c39bff;
+ --syn-num: #ffc46b;
+ --syn-com: #9a8aa0;
+ --syn-pre: #8fdca0;
+ /* Medya önizlemesi (her temada koyu) */
+ --media-bg: #05040a;
+ --media-scrim: rgba(0, 0, 0, .55);
+ --on-media-rgb: 255 255 255;
+ --accent-soft: rgb(var(--accent-rgb) / .14);
--radius: 14px;
--radius-sm: 10px;
--rail-w: 216px;
--dock-w: 372px;
- /* Panel koyu: tarayıcının kendi çizdiği denetimler (sayı okları, kaydırma
- çubukları, stilsiz kutular) de koyu çizilsin (#622) */
+ /* Tarayıcının kendi çizdiği denetimler (sayı okları, kaydırma çubukları,
+ stilsiz kutular) temanın tonunda çizilsin (#622); aydınlık temalarda
+ theme-boot.js `light` yazar (#770) */
color-scheme: dark;
}
@@ -37,8 +98,8 @@
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background:
- radial-gradient(1100px 620px at 88% -14%, #3d1016 0%, transparent 62%),
- radial-gradient(900px 520px at 4% 108%, #1d1030 0%, transparent 60%),
+ radial-gradient(1100px 620px at 88% -14%, var(--glow-a) 0%, transparent 62%),
+ radial-gradient(900px 520px at 4% 108%, var(--glow-b) 0%, transparent 60%),
var(--bg);
color: var(--text);
height: 100vh;
@@ -57,14 +118,14 @@ body {
align-items: center;
gap: 18px;
padding: 11px 18px;
- background: linear-gradient(180deg, rgba(255,255,255,.045), rgba(255,255,255,.012));
+ background: linear-gradient(180deg, rgb(var(--fg-rgb) / .045), rgb(var(--fg-rgb) / .012));
border-bottom: 1px solid var(--line);
flex-shrink: 0;
}
.brand { display: flex; align-items: center; gap: 11px; flex-shrink: 0; }
.brand-logo {
width: 38px; height: 38px; border-radius: 11px;
- box-shadow: 0 0 20px rgba(225,29,42,.4);
+ box-shadow: 0 0 20px rgb(var(--accent-rgb) / .4);
}
.brand-text { display: flex; flex-direction: column; gap: 1px; }
.wordmark { font-size: 18px; font-weight: 800; letter-spacing: 1.4px; line-height: 1; }
@@ -73,9 +134,9 @@ body {
iri görünüyordu (18px yazının büyük harfi ~13px). */
.wordmark-svg { display: block; height: 16px; width: auto; }
.wordmark-svg .wm-red { fill: var(--accent); }
-.wordmark-svg .wm-white { fill: #ffffff; }
+.wordmark-svg .wm-white { fill: var(--brand-ink); }
/* Eski metnin text-shadow'unun karşılığı; kırmızı kısım aynı parlaklığı korusun */
-.wordmark-svg .wm-red { filter: drop-shadow(0 0 7px rgba(225,29,42,.5)); }
+.wordmark-svg .wm-red { filter: drop-shadow(0 0 7px rgb(var(--accent-rgb) / .5)); }
.brand .sub { font-size: 10.5px; color: var(--muted2); letter-spacing: .25px; white-space: nowrap; }
/* --- arama --- */
@@ -83,7 +144,7 @@ body {
position: relative; flex: 1; max-width: 420px;
display: flex; align-items: center; gap: 8px;
padding: 0 10px; height: 36px;
- background: rgba(0,0,0,.28); border: 1px solid var(--line);
+ background: rgb(var(--shade-rgb) / .28); border: 1px solid var(--line);
border-radius: var(--radius-sm); transition: border-color .15s, box-shadow .15s;
}
.topsearch:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
@@ -94,15 +155,15 @@ body {
}
.topsearch input::placeholder { color: var(--muted2); }
.ts-kbd {
- font-size: 9.5px; letter-spacing: .3px; color: var(--muted2);
- background: rgba(255,255,255,.05); border: 1px solid var(--line);
+ font-size: 9.5px; letter-spacing: .3px; color: var(--muted);
+ background: rgb(var(--fg-rgb) / .05); border: 1px solid var(--line);
border-radius: 5px; padding: 2px 5px; white-space: nowrap;
}
.search-results {
position: absolute; z-index: 200; top: calc(100% + 7px); left: 0; right: 0;
max-height: 380px; overflow-y: auto; padding: 6px;
- background: rgba(20, 14, 22, .985); border: 1px solid var(--line2);
- border-radius: 12px; box-shadow: 0 22px 55px rgba(0,0,0,.6);
+ background: var(--popover); border: 1px solid var(--line2);
+ border-radius: 12px; box-shadow: 0 22px 55px rgb(var(--shadow-rgb) / .6);
display: flex; flex-direction: column; gap: 2px;
}
.sr-item {
@@ -110,7 +171,7 @@ body {
border-radius: 8px; cursor: pointer; border: none; background: none;
color: var(--text); font: inherit; text-align: left; width: 100%;
}
-.sr-item:hover, .sr-item.active { background: rgba(225,29,42,.16); }
+.sr-item:hover, .sr-item.active { background: rgb(var(--accent-rgb) / .16); }
.sr-ico { font-size: 14px; flex-shrink: 0; }
.sr-main { flex: 1; min-width: 0; }
.sr-label { font-size: 12.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@@ -153,7 +214,7 @@ body {
.studio-status .svi, .autovj-status .svi, .md-ok .svi, .mded-ok .svi { width: 1.1em; height: 1.1em; }
.lighting-status.ok .svi, .md-ok .svi, .mded-ok .svi { color: var(--ok); }
.autovj-status .svi-warning, .rec-status .svi-warning, .export-status .svi-warning, .audio-state .svi-warning,
-.lighting-status .svi-warning { color: var(--warn, #ffc44d); }
+.lighting-status .svi-warning { color: var(--warn); }
/* Katlanır bölüm oku: kapalıyken sağa, açıkken aşağı */
.fold-caret { display: inline-flex; margin-right: .35em; transition: transform .15s; }
.fold-caret .svi { width: 10px; height: 10px; vertical-align: 0; }
@@ -170,30 +231,30 @@ body {
}
.btn:hover { filter: brightness(1.18); border-color: var(--line2); }
.btn:disabled { opacity: .4; cursor: not-allowed; }
-.btn.primary { background: linear-gradient(135deg, var(--accent), var(--accent2)); border-color: transparent; }
-.btn.primary:hover { box-shadow: 0 4px 18px rgba(225,29,42,.35); }
+.btn.primary { background: linear-gradient(135deg, var(--accent), var(--accent-grad)); border-color: transparent; color: var(--on-accent); }
+.btn.primary:hover { box-shadow: 0 4px 18px rgb(var(--accent-rgb) / .35); }
/* Katmanlara Geç — boş yığında tek eylem; diğer small/ghost düğmelerden ayrışsın */
.btn.layers-cta {
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
width: 100%; margin-top: 10px; padding: 12px 18px; font-size: 13.5px; font-weight: 600;
letter-spacing: 0.01em; border-radius: 10px;
- box-shadow: 0 6px 22px rgba(225,29,42,.28), inset 0 1px 0 rgba(255,255,255,.12);
+ box-shadow: 0 6px 22px rgb(var(--accent-rgb) / .28), inset 0 1px 0 rgb(var(--fg-rgb) / .12);
}
.btn.layers-cta:hover {
filter: brightness(1.1);
- box-shadow: 0 8px 28px rgba(225,29,42,.42), inset 0 1px 0 rgba(255,255,255,.16);
+ box-shadow: 0 8px 28px rgb(var(--accent-rgb) / .42), inset 0 1px 0 rgb(var(--fg-rgb) / .16);
}
.btn.layers-cta .svi { width: 1.35em; height: 1.35em; }
/* Use Layer Stack — master switch; not the same as other toggles in this card */
.ctrl.layer-stack-toggle {
margin: 0 0 14px; padding: 12px 14px; border-radius: 10px;
- border: 1px solid rgba(225,29,42,.38);
+ border: 1px solid rgb(var(--accent-rgb) / .38);
border-left: 3px solid var(--accent);
- background: linear-gradient(135deg, var(--accent-soft), rgba(225,29,42,.06));
- box-shadow: 0 4px 16px rgba(225,29,42,.12), inset 0 1px 0 rgba(255,255,255,.06);
+ background: linear-gradient(135deg, var(--accent-soft), rgb(var(--accent-rgb) / .06));
+ box-shadow: 0 4px 16px rgb(var(--accent-rgb) / .12), inset 0 1px 0 rgb(var(--fg-rgb) / .06);
}
.ctrl.layer-stack-toggle > .row > label.lbl {
- font-size: 13px; font-weight: 700; letter-spacing: .02em; color: #ffc7cd;
+ font-size: 13px; font-weight: 700; letter-spacing: .02em; color: var(--accent-label);
}
.ctrl.layer-stack-toggle .switch { width: 46px; height: 24px; }
.ctrl.layer-stack-toggle .switch .track { border-radius: 12px; }
@@ -201,8 +262,8 @@ body {
.ctrl.layer-stack-toggle .switch input:checked + .track::after { left: 24px; }
.btn.ghost { background: transparent; }
.btn.small { padding: 6px 11px; font-size: 11.5px; }
-.btn.danger { color: #ff8a8a; border-color: rgba(255,80,80,.35); }
-.btn.danger:hover { background: rgba(255,60,60,.12); }
+.btn.danger { color: var(--danger-text); border-color: rgb(var(--danger-rgb) / .35); }
+.btn.danger:hover { background: rgb(var(--danger-rgb) / .12); }
.icon-btn {
width: 32px; height: 32px; flex-shrink: 0; display: grid; place-items: center;
@@ -215,18 +276,18 @@ body {
/* ============================ UYARI BANDI ============================ */
.banner {
display: flex; align-items: flex-start; gap: 12px; padding: 11px 18px;
- background: linear-gradient(90deg, rgba(255,170,40,.16), rgba(255,90,90,.10));
- border-bottom: 1px solid rgba(255,170,40,.35); color: #ffd9a8; flex-shrink: 0;
+ background: linear-gradient(90deg, rgb(var(--warn-rgb) / .16), rgb(var(--danger-rgb) / .1));
+ border-bottom: 1px solid rgb(var(--warn-rgb) / .35); color: var(--warn-text); flex-shrink: 0;
}
.banner .bicon { font-size: 17px; line-height: 1.4; }
.banner .btext { flex: 1; font-size: 12px; line-height: 1.5; }
-.banner .btext strong { color: #ffe; }
-.banner .bhelp { margin-top: 4px; color: #e9d3b0; font-size: 11px; }
+.banner .btext strong { color: var(--text-strong); }
+.banner .bhelp { margin-top: 4px; color: var(--warn-text); font-size: 11px; }
.banner-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* Başka kopyaların listesi (#564): yol uzun olabiliyor, satırı taşırmasın */
.banner .copies-list { list-style: none; margin: 4px 0 0; padding: 0; }
.banner .copies-list li + li { margin-top: 4px; }
-.banner .copy-path { color: #cdb99a; font-family: ui-monospace, Consolas, monospace; font-size: 10.5px; word-break: break-all; }
+.banner .copy-path { color: var(--muted); font-family: ui-monospace, Consolas, monospace; font-size: 10.5px; word-break: break-all; }
/* ============================ ÇALIŞMA ALANI ============================ */
.workspace {
@@ -238,7 +299,7 @@ body {
/* -------- Sol: kategori rayı -------- */
.nav-rail {
border-right: 1px solid var(--line);
- background: linear-gradient(180deg, rgba(0,0,0,.22), rgba(0,0,0,.34));
+ background: linear-gradient(180deg, rgb(var(--shade-rgb) / .22), rgb(var(--shade-rgb) / .34));
padding: 12px 10px; overflow-y: auto;
display: flex; flex-direction: column; gap: 3px;
}
@@ -253,10 +314,10 @@ body {
font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer;
text-align: left; width: 100%; transition: .14s; position: relative;
}
-.nav-item:hover { background: rgba(255,255,255,.045); color: var(--text); }
+.nav-item:hover { background: rgb(var(--fg-rgb) / .045); color: var(--text); }
.nav-item.active {
- background: linear-gradient(90deg, rgba(225,29,42,.22), rgba(225,29,42,.05));
- border-color: rgba(225,29,42,.35); color: #fff;
+ background: linear-gradient(90deg, rgb(var(--accent-rgb) / .22), rgb(var(--accent-rgb) / .05));
+ border-color: rgb(var(--accent-rgb) / .35); color: var(--text-strong);
}
.nav-item.active::before {
content: ''; position: absolute; left: -10px; top: 50%; transform: translateY(-50%);
@@ -268,9 +329,9 @@ body {
.nav-badge {
font-size: 9.5px; font-weight: 700; min-width: 18px; text-align: center;
padding: 2px 5px; border-radius: 20px;
- background: rgba(225,29,42,.22); color: #ff9aa3; border: 1px solid rgba(225,29,42,.3);
+ background: rgb(var(--accent-rgb) / .22); color: var(--accent-text); border: 1px solid rgb(var(--accent-rgb) / .3);
}
-.nav-item.active .nav-badge { background: rgba(255,255,255,.16); color: #fff; border-color: transparent; }
+.nav-item.active .nav-badge { background: rgb(var(--fg-rgb) / .16); color: var(--text-strong); border-color: transparent; }
.nav-spacer { flex: 1; min-height: 8px; }
.nav-foot { font-size: 10px; color: var(--muted2); padding: 8px 11px; line-height: 1.5; }
@@ -301,8 +362,8 @@ body {
content: ''; position: absolute; width: 11px; height: 11px; border-radius: 50%;
background: var(--muted); top: 2px; left: 2px; transition: .18s;
}
-.adv-toggle input:checked + .track { background: linear-gradient(135deg, var(--accent), var(--accent2)); border-color: transparent; }
-.adv-toggle input:checked + .track::after { left: 15px; background: #fff; }
+.adv-toggle input:checked + .track { background: linear-gradient(135deg, var(--accent), var(--accent-grad)); border-color: transparent; }
+.adv-toggle input:checked + .track::after { left: 15px; background: var(--knob); }
.sections {
flex: 1; min-height: 0; min-width: 0; overflow-y: auto; overflow-x: hidden;
@@ -326,7 +387,7 @@ body {
/* -------- Kartlar -------- */
.card {
- background: linear-gradient(180deg, var(--card), rgba(16,11,18,.9));
+ background: linear-gradient(180deg, var(--card), var(--card-end));
border: 1px solid var(--line); border-radius: var(--radius);
padding: 14px 16px 15px;
/* min-width: auto ızgarayı etiketin boyuna açıp yatay kaydırma çıkarıyordu. */
@@ -340,7 +401,7 @@ body {
.card-head .ico {
font-size: 15px; width: 28px; height: 28px; flex-shrink: 0;
display: grid; place-items: center; border-radius: 8px;
- background: rgba(255,255,255,.05); border: 1px solid var(--line);
+ background: rgb(var(--fg-rgb) / .05); border: 1px solid var(--line);
}
.card-head .ch-main { flex: 1; min-width: 0; }
.card-head h3 { font-size: 13.5px; font-weight: 700; letter-spacing: .1px; }
@@ -348,7 +409,7 @@ body {
.card-head .ch-actions { display: flex; align-items: center; gap: 5px; flex-shrink: 0; }
.chip-mod {
font-size: 9.5px; font-weight: 700; padding: 2px 7px; border-radius: 20px;
- background: rgba(225,29,42,.16); color: #ff9aa3; border: 1px solid rgba(225,29,42,.28);
+ background: rgb(var(--accent-rgb) / .16); color: var(--accent-text); border: 1px solid rgb(var(--accent-rgb) / .28);
white-space: nowrap;
}
@@ -424,7 +485,7 @@ body {
.adv-summary {
display: flex; align-items: center; gap: 8px; width: 100%;
padding: 8px 11px; border-radius: var(--radius-sm);
- background: rgba(255,255,255,.035); border: 1px solid var(--line);
+ background: rgb(var(--fg-rgb) / .035); border: 1px solid var(--line);
color: var(--muted); font: inherit; font-size: 11.5px; font-weight: 600;
cursor: pointer; transition: .15s;
}
@@ -467,7 +528,7 @@ body {
.ctrl-reset:hover { color: var(--accent2); background: var(--accent-soft); }
.flash { animation: ctrlFlash 1.6s ease-out; border-radius: var(--radius-sm); }
@keyframes ctrlFlash {
- 0%, 55% { box-shadow: 0 0 0 2px var(--accent2), 0 0 22px rgba(255,77,94,.35); background: rgba(225,29,42,.09); }
+ 0%, 55% { box-shadow: 0 0 0 2px var(--accent2), 0 0 22px rgb(var(--accent-rgb) / .35); background: rgb(var(--accent-rgb) / .09); }
100% { box-shadow: 0 0 0 0 transparent; background: transparent; }
}
@@ -477,11 +538,11 @@ input[type=range] {
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none; width: 15px; height: 15px; border-radius: 50%;
- background: linear-gradient(135deg, var(--accent), var(--accent2));
- cursor: pointer; box-shadow: 0 0 0 3px rgba(225,29,42,.16);
+ background: linear-gradient(135deg, var(--accent), var(--accent-grad));
+ cursor: pointer; box-shadow: 0 0 0 3px rgb(var(--accent-rgb) / .16);
transition: box-shadow .15s;
}
-input[type=range]::-webkit-slider-thumb:hover { box-shadow: 0 0 0 5px rgba(225,29,42,.24); }
+input[type=range]::-webkit-slider-thumb:hover { box-shadow: 0 0 0 5px rgb(var(--accent-rgb) / .24); }
/* toggle */
.switch { position: relative; width: 40px; height: 22px; flex-shrink: 0; }
@@ -501,8 +562,8 @@ input[type=range]::-webkit-slider-thumb:hover { box-shadow: 0 0 0 5px rgba(225,2
content: ''; position: absolute; width: 16px; height: 16px; border-radius: 50%;
background: var(--muted); top: 2px; left: 2px; transition: .18s;
}
-.switch input:checked + .track { background: linear-gradient(135deg, var(--accent), var(--accent2)); border-color: transparent; }
-.switch input:checked + .track::after { left: 20px; background: #fff; }
+.switch input:checked + .track { background: linear-gradient(135deg, var(--accent), var(--accent-grad)); border-color: transparent; }
+.switch input:checked + .track::after { left: 20px; background: var(--knob); }
/* color */
input[type=color] {
@@ -524,8 +585,8 @@ input[type=color]::-webkit-color-swatch { border: none; border-radius: 6px; }
}
.segment button:hover { color: var(--text); border-color: var(--line2); }
.segment button.active {
- background: linear-gradient(135deg, var(--accent), var(--accent2));
- color: #fff; border-color: transparent;
+ background: linear-gradient(135deg, var(--accent), var(--accent-grad));
+ color: var(--on-accent); border-color: transparent;
}
select {
@@ -600,7 +661,7 @@ select:focus { border-color: var(--accent); }
width: 100%;
height: 64px;
object-fit: contain;
- background: #0c0a0d;
+ background: var(--media-bg);
border-radius: 6px;
display: block;
}
@@ -619,7 +680,7 @@ video.logo-lib-thumb { pointer-events: none; }
left: 10px;
font-size: 9px;
background: var(--accent);
- color: #fff;
+ color: var(--on-accent);
border-radius: 4px;
padding: 1px 4px;
pointer-events: none;
@@ -636,15 +697,15 @@ video.logo-lib-thumb { pointer-events: none; }
justify-content: center;
border: 0;
border-radius: 50%;
- background: rgba(12, 8, 10, 0.88);
- color: #fff;
+ background: var(--media-scrim);
+ color: rgb(var(--on-media-rgb));
cursor: pointer;
padding: 0;
line-height: 0;
}
-.logo-lib-del .svi { width: 12px; height: 12px; color: #fff; }
+.logo-lib-del .svi { width: 12px; height: 12px; color: rgb(var(--on-media-rgb)); }
.logo-lib-del:hover,
-.logo-lib-del:focus-visible { background: #e23b3b; color: #fff; }
+.logo-lib-del:focus-visible { background: var(--danger); color: var(--on-solid); }
.subctrls { border-left: 2px solid var(--line2); padding-left: 12px; margin-top: 6px; }
@@ -675,7 +736,7 @@ video.logo-lib-thumb { pointer-events: none; }
.up-toolbar { display: flex; gap: 7px; flex-wrap: wrap; margin-top: 10px; }
.settings-io-panel {
padding: 12px; border: 1px solid var(--line2); border-radius: 11px;
- background: rgba(255,255,255,.03);
+ background: rgb(var(--fg-rgb) / .03);
}
.settings-io-note { color: var(--muted2); font-size: 11px; line-height: 1.55; }
.settings-io-panel .up-toolbar { margin-top: 12px; }
@@ -688,7 +749,7 @@ video.logo-lib-thumb { pointer-events: none; }
.img-head { display: flex; gap: 10px; align-items: center; }
.img-thumb {
width: 50px; height: 50px; object-fit: contain; border-radius: 9px;
- background: #0c0a0d; border: 1px solid var(--line); flex-shrink: 0;
+ background: var(--media-bg); border: 1px solid var(--line); flex-shrink: 0;
}
.img-headmain { flex: 1; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.img-body { margin-top: 10px; padding-top: 9px; border-top: 1px dashed var(--line); }
@@ -706,12 +767,12 @@ video.logo-lib-thumb { pointer-events: none; }
}
.export-progress i {
display: block; height: 100%; width: 0%;
- background: linear-gradient(90deg, var(--accent), var(--accent2));
+ background: linear-gradient(90deg, var(--accent), var(--accent-grad));
transition: width .12s linear;
}
.export-status { margin-top: 8px; font-size: 11.5px; color: var(--muted); word-break: break-all; }
.export-status.ok { color: var(--ok); }
-.export-status.err { color: #ff7a7a; }
+.export-status.err { color: var(--danger-text); }
/* ---------- Çoklu ses kaynağı seçici ---------- */
.source-list { display: flex; flex-direction: column; gap: 5px; margin-top: 8px; }
@@ -735,7 +796,7 @@ video.logo-lib-thumb { pointer-events: none; }
}
.source-meter i {
display: block; height: 100%; width: 100%;
- background: linear-gradient(90deg, var(--ok), #ffd54a 75%, #ff5e6c);
+ background: linear-gradient(90deg, var(--ok), var(--meter-mid) 75%, var(--meter-hot));
/* Geçiş yok: düşüş panelde kare kare yürütülüyor */
transform: scaleX(0); transform-origin: 0 50%;
}
@@ -743,7 +804,7 @@ video.logo-lib-thumb { pointer-events: none; }
/* ============================ SAĞ DOCK ============================ */
.dock {
border-left: 1px solid var(--line);
- background: linear-gradient(180deg, rgba(0,0,0,.22), rgba(0,0,0,.34));
+ background: linear-gradient(180deg, rgb(var(--shade-rgb) / .22), rgb(var(--shade-rgb) / .34));
display: flex; flex-direction: column; min-height: 0; overflow-y: auto; overflow-x: hidden;
}
.dock-block { padding: 14px 15px; border-bottom: 1px solid var(--line); flex-shrink: 0; }
@@ -760,13 +821,13 @@ video.logo-lib-thumb { pointer-events: none; }
background: var(--card2); border: 1px solid var(--line); color: var(--muted);
white-space: nowrap;
}
-.pill.live { background: rgba(67,217,138,.14); border-color: rgba(67,217,138,.35); color: var(--ok); }
+.pill.live { background: rgb(var(--ok-rgb) / .14); border-color: rgb(var(--ok-rgb) / .35); color: var(--ok); }
/* --- önizleme --- */
.preview-stage {
position: relative; width: 100%; aspect-ratio: 16 / 9;
- border-radius: 11px; overflow: hidden; background: #05040a;
- border: 1px solid var(--line2); box-shadow: inset 0 0 40px rgba(0,0,0,.6);
+ border-radius: 11px; overflow: hidden; background: var(--media-bg);
+ border: 1px solid var(--line2); box-shadow: inset 0 0 40px rgb(var(--shadow-rgb) / .6);
}
#pvStage { position: absolute; inset: 0; isolation: isolate; }
.preview-stage canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
@@ -778,13 +839,13 @@ video.logo-lib-thumb { pointer-events: none; }
position: absolute; right: 7px; bottom: 6px; z-index: 5;
font-size: 9px; font-weight: 700; letter-spacing: .05em;
padding: 2px 6px; border-radius: 5px;
- background: rgba(0,0,0,.55); color: rgba(255,255,255,.55);
+ background: var(--media-scrim); color: rgb(var(--on-media-rgb) / .55);
}
/* Duraklatma işareti CSS'te: ikon setindeki "pause" (icons.js), sabit renkle
data: URL'si olarak (#665). */
.preview-stage.paused::after {
content: ''; position: absolute; inset: 0; z-index: 6;
- background: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22%3E%3Cpath d=%22M7.5 5h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-12a1 1 0 0 1 1 -1z%22 fill=%22rgba(255,255,255,.75)%22/%3E%3Cpath d=%22M14.5 5h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-12a1 1 0 0 1 1 -1z%22 fill=%22rgba(255,255,255,.75)%22/%3E%3C/svg%3E") center / 38px no-repeat, rgba(5,4,10,.62);
+ background: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22%3E%3Cpath d=%22M7.5 5h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-12a1 1 0 0 1 1 -1z%22 fill=%22rgba(255,255,255,.75)%22/%3E%3Cpath d=%22M14.5 5h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-12a1 1 0 0 1 1 -1z%22 fill=%22rgba(255,255,255,.75)%22/%3E%3C/svg%3E") center / 38px no-repeat, var(--media-scrim);
}
/* --- seviye göstergeleri --- */
@@ -799,7 +860,7 @@ video.logo-lib-thumb { pointer-events: none; }
Gradyan kutuya göre çizildiği için görünüm aynı. */
.meter .bar i {
display: block; height: 100%; width: 100%;
- background: linear-gradient(90deg, var(--ok), #ffd54a 70%, #ff5e6c);
+ background: linear-gradient(90deg, var(--ok), var(--meter-mid) 70%, var(--meter-hot));
transform: scaleX(0); transform-origin: 0 50%;
transition: transform .05s linear;
will-change: transform;
@@ -811,7 +872,7 @@ video.logo-lib-thumb { pointer-events: none; }
min-width: 0; overflow: hidden; text-overflow: ellipsis; text-align: right;
}
.audio-state.ok { color: var(--ok); }
-.audio-state.err { color: #ff7a7a; }
+.audio-state.err { color: var(--danger-text); }
.audio-state.warn { color: var(--warn); }
/* --- sahneler --- */
@@ -855,7 +916,7 @@ video.logo-lib-thumb { pointer-events: none; }
}
.scene-thumb::after {
content: ''; position: absolute; left: 3px; right: 3px; bottom: 3px; height: 30%;
- background: repeating-linear-gradient(90deg, rgba(255,255,255,.75) 0 2px, transparent 2px 4px);
+ background: repeating-linear-gradient(90deg, rgb(var(--on-media-rgb) / .75) 0 2px, transparent 2px 4px);
opacity: .55; border-radius: 2px;
}
.scene-main { flex: 1; min-width: 0; }
@@ -872,7 +933,7 @@ video.logo-lib-thumb { pointer-events: none; }
/* ============================ ALT ÇUBUK ============================ */
.footer {
display: flex; align-items: center; justify-content: space-between; gap: 16px;
- padding: 9px 18px; border-top: 1px solid var(--line); background: rgba(0,0,0,.28);
+ padding: 9px 18px; border-top: 1px solid var(--line); background: rgb(var(--shade-rgb) / .28);
flex-shrink: 0;
}
.footer-end { display: flex; align-items: center; gap: 12px; margin-left: auto; min-width: 0; }
@@ -892,14 +953,14 @@ kbd { background: var(--card2); border: 1px solid var(--line); border-radius: 5p
kurulumda başka ekranda açılabiliyordu; bunlar panelin içinde kalır. */
.ask-backdrop {
position: fixed; inset: 0; z-index: 400; display: grid; place-items: center;
- background: rgba(3, 3, 8, .62); backdrop-filter: blur(3px);
+ background: var(--scrim); backdrop-filter: blur(3px);
animation: askIn .12s ease-out;
}
@keyframes askIn { from { opacity: 0; } to { opacity: 1; } }
.ask-panel {
width: min(420px, calc(100vw - 40px)); padding: 20px;
border: 1px solid var(--line2); border-radius: 14px;
- background: var(--card); box-shadow: 0 28px 80px rgba(0, 0, 0, .6);
+ background: var(--card); box-shadow: 0 28px 80px rgb(var(--shadow-rgb) / .6);
}
.ask-title { font-size: 15px; font-weight: 700; margin-bottom: 8px; }
.ask-text { font-size: 12.5px; color: var(--muted); line-height: 1.6; }
@@ -920,20 +981,20 @@ kbd { background: var(--card2); border: 1px solid var(--line); border-radius: 5p
.lyr-editor-code {
flex: 1 1 auto; min-height: 0; margin-top: 8px;
display: grid; grid-template-rows: minmax(0, 1fr);
- border: 1px solid var(--line); border-radius: 10px; background: #12080c; overflow: hidden;
+ border: 1px solid var(--line); border-radius: 10px; background: var(--code-bg); overflow: hidden;
}
.lyr-editor-code:focus-within { border-color: var(--accent); }
.lyr-editor-paint {
grid-area: 1 / 1; margin: 0; overflow: auto; pointer-events: none;
- scrollbar-width: none; color: #f2f2f4;
+ scrollbar-width: none; color: var(--code-fg);
}
.lyr-editor-paint::-webkit-scrollbar { width: 0; height: 0; }
-.lyr-k-time { color: #ffd23f; }
-.lyr-k-word { color: #8ec8ff; }
-.lyr-k-meta, .lyr-k-meta-val { color: #d2b4ff; }
-.lyr-k-meta-val { color: #efe4ff; }
-.lyr-k-lyric { color: #f4f4f6; }
-.lyr-k-arrow, .lyr-k-index { color: #9aa3b2; }
+.lyr-k-time { color: var(--syn-num); }
+.lyr-k-word { color: var(--syn-type); }
+.lyr-k-meta, .lyr-k-meta-val { color: var(--syn-fn); }
+.lyr-k-meta-val { color: var(--code-fg); }
+.lyr-k-lyric { color: var(--code-fg); }
+.lyr-k-arrow, .lyr-k-index { color: var(--syn-com); }
.lyr-editor .ask-actions { margin-top: 12px; }
@media (max-width: 640px) {
.lyr-editor-fields { grid-template-columns: 1fr; }
@@ -944,13 +1005,13 @@ kbd { background: var(--card2); border: 1px solid var(--line); border-radius: 5p
z-index: 420; max-width: min(620px, calc(100vw - 40px));
padding: 11px 16px; border-radius: var(--radius-sm);
background: var(--card2); border: 1px solid var(--line2); color: var(--text);
- font-size: 12.5px; line-height: 1.5; box-shadow: 0 14px 40px rgba(0, 0, 0, .5);
+ font-size: 12.5px; line-height: 1.5; box-shadow: 0 14px 40px rgb(var(--shadow-rgb) / .5);
transition: opacity .2s, transform .2s;
}
.toast.hidden { opacity: 0; pointer-events: none; transform: translateX(-50%) translateY(8px); }
-.toast.ok { border-color: rgba(67, 217, 138, .45); color: #9df0c4; }
-.toast.warn { border-color: rgba(255, 196, 77, .45); color: #f3cf82; }
-.toast.err { border-color: rgba(255, 90, 90, .5); color: #ff9d9d; }
+.toast.ok { border-color: rgb(var(--ok-rgb) / .45); color: var(--ok-text); }
+.toast.warn { border-color: rgb(var(--warn-rgb) / .45); color: var(--warn-text); }
+.toast.err { border-color: rgb(var(--danger-rgb) / .5); color: var(--danger-text); }
/* ============================ DYNAMIC LIGHTING ============================ */
.lighting-panel { display: grid; gap: 8px; }
@@ -958,10 +1019,10 @@ kbd { background: var(--card2); border: 1px solid var(--line); border-radius: 5p
padding: 9px 10px; border: 1px solid var(--line); border-radius: 9px;
background: var(--card2); color: var(--muted); font-size: 11.5px; line-height: 1.45;
}
-.lighting-status.ok { border-color: rgba(54, 211, 153, .42); color: #7ee2bd; }
+.lighting-status.ok { border-color: rgb(var(--ok-rgb) / .42); color: var(--ok-text); }
.lighting-priority-note {
- padding: 10px 11px; border: 1px solid rgba(255, 196, 77, .35); border-radius: 9px;
- background: rgba(255, 196, 77, .08); color: #f3cf82; font-size: 11px; line-height: 1.5;
+ padding: 10px 11px; border: 1px solid rgb(var(--warn-rgb) / .35); border-radius: 9px;
+ background: rgb(var(--warn-rgb) / .08); color: var(--warn-text); font-size: 11px; line-height: 1.5;
}
.lighting-device { margin-top: 2px; }
.lighting-panel select { width: 100%; }
@@ -980,11 +1041,11 @@ kbd { background: var(--card2); border: 1px solid var(--line); border-radius: 5p
.lighting-subtitle {
margin: 10px 0 0; padding: 8px 10px; border-left: 3px solid var(--accent);
border-radius: 7px; background: var(--accent-soft);
- color: #ffc7cd; font-size: 11.5px; font-weight: 700; letter-spacing: .02em;
+ color: var(--accent-label); font-size: 11.5px; font-weight: 700; letter-spacing: .02em;
}
.lighting-mode-help {
padding: 9px 10px; border: 1px solid var(--line2); border-radius: 9px;
- background: rgba(255,255,255,.03); color: var(--muted2); font-size: 10.5px; line-height: 1.5;
+ background: rgb(var(--fg-rgb) / .03); color: var(--muted2); font-size: 10.5px; line-height: 1.5;
}
.lighting-select-wrap { position: relative; margin-top: 7px; outline: none; }
.lighting-select-button {
@@ -1002,17 +1063,17 @@ kbd { background: var(--card2); border: 1px solid var(--line); border-radius: 5p
.lighting-select-menu {
display: none; position: absolute; z-index: 120; left: 0; right: 0; top: calc(100% + 6px);
max-height: 340px; overflow-y: auto; padding: 6px; border: 1px solid var(--line2);
- border-radius: 11px; background: rgba(20, 14, 22, .985); box-shadow: 0 18px 45px rgba(0,0,0,.5);
+ border-radius: 11px; background: var(--popover); box-shadow: 0 18px 45px rgb(var(--shadow-rgb) / .5);
}
.lighting-select-wrap.open .lighting-select-menu { display: grid; gap: 3px; }
.lighting-select-option {
width: 100%; display: grid; gap: 3px; padding: 9px 10px; border: 0; border-radius: 8px;
background: transparent; color: var(--text); text-align: left; cursor: pointer; font: inherit;
}
-.lighting-select-option:hover { background: rgba(225,29,42,.13); }
+.lighting-select-option:hover { background: rgb(var(--accent-rgb) / .13); }
.lighting-select-option.active {
- background: linear-gradient(90deg, rgba(225,29,42,.3), rgba(225,29,42,.08));
- box-shadow: inset 0 0 0 1px rgba(225,29,42,.35);
+ background: linear-gradient(90deg, rgb(var(--accent-rgb) / .3), rgb(var(--accent-rgb) / .08));
+ box-shadow: inset 0 0 0 1px rgb(var(--accent-rgb) / .35);
}
.lighting-option-label { font-size: 11.5px; font-weight: 700; }
.lighting-option-desc { color: var(--muted2); font-size: 10px; line-height: 1.35; }
@@ -1023,7 +1084,7 @@ kbd { background: var(--card2); border: 1px solid var(--line); border-radius: 5p
margin: 0 0 4px; padding: 10px 12px; border-radius: 9px;
border-left: 3px solid var(--accent);
background: var(--accent-soft);
- color: #ffc7cd; font-size: 11.5px; line-height: 1.5; font-weight: 600;
+ color: var(--accent-label); font-size: 11.5px; line-height: 1.5; font-weight: 600;
}
.lighting-tagged { display: grid; gap: 4px; }
.lighting-backends { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
@@ -1080,7 +1141,7 @@ kbd { background: var(--card2); border: 1px solid var(--line); border-radius: 5p
.display-menu {
position: absolute; z-index: 40; top: calc(100% + 5px); left: 0; min-width: 240px;
background: var(--surface); border: 1px solid var(--line2); border-radius: 11px;
- padding: 6px; box-shadow: 0 14px 40px rgba(0,0,0,.55);
+ padding: 6px; box-shadow: 0 14px 40px rgb(var(--shadow-rgb) / .55);
display: flex; flex-direction: column; gap: 2px;
}
.display-menu.hidden { display: none; }
@@ -1096,7 +1157,7 @@ kbd { background: var(--card2); border: 1px solid var(--line); border-radius: 5p
display: flex; align-items: center; gap: 8px;
padding: 8px 10px; cursor: pointer; font-size: 12.5px; font-weight: 600;
}
-.dm-pip:hover { background: rgba(255,255,255,.04); }
+.dm-pip:hover { background: rgb(var(--fg-rgb) / .04); }
.dm-pip input { accent-color: var(--accent); width: 15px; height: 15px; }
.display-menu .dm-sub { color: var(--muted2); font-size: 10.5px; margin-left: auto; }
@@ -1124,8 +1185,8 @@ kbd { background: var(--card2); border: 1px solid var(--line); border-radius: 5p
object-fit: cover;
border-radius: 8px;
margin: 6px 0 2px;
- background: #0b0b12;
- border: 1px solid rgba(255, 255, 255, 0.08);
+ background: var(--media-bg);
+ border: 1px solid rgb(var(--fg-rgb) / .08);
}
@@ -1174,7 +1235,7 @@ input.num:focus { border-color: var(--accent); }
.tl-panel.tl-full {
position: fixed; inset: 10px; z-index: 950; overflow: auto;
background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
- padding: 12px 14px; box-shadow: 0 24px 70px rgba(0, 0, 0, .65);
+ padding: 12px 14px; box-shadow: 0 24px 70px rgb(var(--shadow-rgb) / .65);
}
.tl-panel.tl-full .tl-body { height: calc(100vh - 250px) !important; min-height: 220px; resize: none; }
body.tl-full-open { overflow: hidden; }
@@ -1208,10 +1269,10 @@ input[type=color].tl-swatch.big { width: 34px; height: 24px; }
border-radius: 4px; border: 1px solid var(--line); background: var(--card2); color: var(--muted); cursor: pointer;
}
.tl-tog:hover { color: var(--text); border-color: var(--line2); }
-.tl-tog.m.on { background: #6a4a52; color: #fff; border-color: #8a6f79; }
-.tl-tog.s.on { background: #d7a100; color: #1b141f; border-color: #ffc44d; }
+.tl-tog.m.on { background: var(--off); color: var(--text-strong); border-color: var(--muted2); }
+.tl-tog.s.on { background: var(--solo); color: var(--on-solo); border-color: var(--warn); }
.tl-tog.l.on { background: var(--card3); color: var(--text); border-color: var(--line2); }
-.tl-canvas-wrap { flex: 1; min-width: 0; background: rgba(255,255,255,.015); }
+.tl-canvas-wrap { flex: 1; min-width: 0; background: rgb(var(--fg-rgb) / .015); }
.tl-canvas { display: block; width: 100%; cursor: crosshair; }
.tl-actions { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }
.tl-inspector { border-top: 1px solid var(--line); padding-top: 8px; margin-top: 8px; }
@@ -1262,7 +1323,7 @@ input[type=color].tl-swatch.big { width: 34px; height: 24px; }
.cd-tab { height: 26px; min-width: 34px; padding: 0 10px; border-radius: 7px; border: 1px solid var(--line); background: var(--card); color: var(--muted); font-size: 12px; font-weight: 600; cursor: pointer; }
.cd-tab:hover { color: var(--text); border-color: var(--line2); }
.cd-tab.on { color: var(--text); border-color: var(--accent); background: var(--accent-soft); }
-.cd-tab.live::after { content: ""; display: inline-block; width: 6px; height: 6px; margin-left: 6px; border-radius: 50%; background: #43d98a; vertical-align: middle; }
+.cd-tab.live::after { content: ""; display: inline-block; width: 6px; height: 6px; margin-left: 6px; border-radius: 50%; background: var(--ok); vertical-align: middle; }
.cd-tab.add { min-width: 28px; padding: 0 8px; }
.cd-grid { display: grid; gap: 4px; }
.cd-corner { font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: var(--muted2); align-self: end; padding: 0 0 4px 6px; }
@@ -1275,38 +1336,39 @@ input[type=color].tl-swatch.big { width: 34px; height: 24px; }
.cd-panel input.cd-colname:hover { border-color: var(--line); }
.cd-panel input.cd-colname:focus { border-color: var(--accent); background: var(--card2); }
.cd-stop, .cd-rowlaunch {
- background: var(--card2, rgba(255,255,255,.04)); border: 1px solid var(--line);
+ background: var(--card2); border: 1px solid var(--line);
color: var(--muted); border-radius: 6px; cursor: pointer; font-size: 11px; padding: 4px 6px;
}
.cd-stop { flex: none; width: 26px; }
.cd-rowlaunch { text-align: left; padding-left: 8px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 150px; }
.cd-stop:hover, .cd-rowlaunch:hover { color: var(--text); border-color: var(--accent); }
.cd-cell {
- --slot: #4f8cff;
+ --slot: var(--slot-default);
position: relative; min-height: 54px; border-radius: 7px; cursor: pointer;
border: 1px dashed var(--line); background: transparent; color: var(--muted2);
display: flex; flex-direction: column; justify-content: center; gap: 2px; padding: 5px 7px 7px 10px;
font-size: 11px; text-align: left; overflow: hidden; font-family: inherit;
}
.cd-cell.empty { align-items: center; justify-content: center; padding: 0; }
-.cd-plus { font-size: 18px; line-height: 1; color: var(--line2); transition: color .12s, transform .12s; }
-.cd-cell.empty:hover { border-color: var(--line2); background: rgba(255,255,255,.02); }
+/* Boş hücrenin "+" işareti tek ekleme ipucu: çizgi rengiyle 1.5:1'di (#770) */
+.cd-plus { font-size: 18px; line-height: 1; color: var(--muted2); transition: color .12s, transform .12s; }
+.cd-cell.empty:hover { border-color: var(--line2); background: rgb(var(--fg-rgb) / .02); }
.cd-cell.empty:hover .cd-plus { color: var(--text); transform: scale(1.15); }
.cd-cell.filled { border: 1px solid color-mix(in srgb, var(--slot) 45%, var(--line)); background: color-mix(in srgb, var(--slot) 14%, var(--card)); color: var(--text); }
.cd-cell.filled:hover { background: color-mix(in srgb, var(--slot) 22%, var(--card)); }
.cd-color { position: absolute; left: 0; top: 0; bottom: 0; width: 4px; }
.cd-name { font-weight: 600; font-size: 11.5px; }
.cd-meta { font-size: 10px; color: var(--muted); min-height: 12px; }
-.cd-progwrap { position: absolute; left: 4px; right: 0; bottom: 0; height: 3px; background: rgba(255,255,255,.05); }
+.cd-progwrap { position: absolute; left: 4px; right: 0; bottom: 0; height: 3px; background: rgb(var(--fg-rgb) / .05); }
.cd-prog { display: block; height: 100%; width: 0%; background: var(--slot); }
/* Hazırlanmış: ateşlenmeyi bekliyor (yanıp söner). Geri sayım köşede. */
.cd-cell.armed { border-color: var(--warn); box-shadow: inset 0 0 0 1px var(--warn); animation: cd-armed .5s steps(2, jump-none) infinite; }
-@keyframes cd-armed { 50% { box-shadow: inset 0 0 0 2px var(--warn), 0 0 10px rgba(255,196,77,.35); } }
+@keyframes cd-armed { 50% { box-shadow: inset 0 0 0 2px var(--warn), 0 0 10px rgb(var(--warn-rgb) / .35); } }
/* Çalıyor: yeşil çerçeve ve parlama; uzaktan da seçilir. */
-.cd-cell.active { border-color: #2ea043; box-shadow: inset 0 0 0 2px #2ea043, 0 0 12px rgba(46,160,67,.35); }
+.cd-cell.active { border-color: var(--cue-live); box-shadow: inset 0 0 0 2px var(--cue-live), 0 0 12px rgb(var(--ok-rgb) / .35); }
.cd-cell.active .cd-name::before {
content: ''; display: inline-block; width: .9em; height: .9em; margin-right: .3em; vertical-align: -.1em;
- background: #43d98a;
+ background: var(--ok);
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22%3E%3Cpath d=%22M7.5 4.8v14.4a.8.8 0 0 0 1.2.7l11.3-7.2a.8.8 0 0 0 0-1.4L8.7 4.1a.8.8 0 0 0-1.2.7z%22 fill=%22%23000%22/%3E%3C/svg%3E") center / contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22%3E%3Cpath d=%22M7.5 4.8v14.4a.8.8 0 0 0 1.2.7l11.3-7.2a.8.8 0 0 0 0-1.4L8.7 4.1a.8.8 0 0 0-1.2.7z%22 fill=%22%23000%22/%3E%3C/svg%3E") center / contain no-repeat;
}
.cd-cell.sel { outline: 2px solid var(--accent); outline-offset: 1px; }
@@ -1330,50 +1392,50 @@ input[type=color].tl-swatch.big { width: 34px; height: 24px; }
Kontrast ve hedef boyutu bilinçli olarak panelin geri kalanından yüksek.
========================================================================== */
.perf-view {
- position: fixed; inset: 0; z-index: 900; background: #05050a;
+ position: fixed; inset: 0; z-index: 900; background: var(--bg);
display: flex; flex-direction: column; gap: 10px; padding: 14px;
outline: none;
}
.perf-head { display: flex; align-items: center; gap: 10px; }
.perf-clock {
- font-size: 30px; font-weight: 700; color: #fff; margin-right: auto;
+ font-size: 30px; font-weight: 700; color: var(--text-strong); margin-right: auto;
font-variant-numeric: tabular-nums; /* sayaç zıplamasın */
}
.perf-btn {
- background: #17171f; border: 1px solid #33333f; color: #e8e8ec;
+ background: var(--card2); border: 1px solid var(--line2); color: var(--text);
border-radius: 8px; padding: 12px 18px; font-size: 15px; cursor: pointer;
}
-.perf-btn:hover { border-color: #6a6a7a; }
-.perf-btn.danger { border-color: #7a2530; color: #ff6b78; }
+.perf-btn:hover { border-color: var(--muted2); }
+.perf-btn.danger { border-color: rgb(var(--danger-rgb) / .45); color: var(--danger-text); }
.perf-grid { flex: 1; display: grid; gap: 8px; min-height: 0; }
.perf-corner { }
.perf-stopcol, .perf-row {
- background: #12121a; border: 1px solid #2a2a36; color: #9a9aa8;
+ background: var(--card); border: 1px solid var(--line); color: var(--muted);
border-radius: 8px; font-size: 15px; cursor: pointer; padding: 8px;
}
.perf-row { text-align: left; padding-left: 14px; font-weight: 600; }
-.perf-row.cursor { border-color: #e11d2a; color: #fff; }
+.perf-row.cursor { border-color: var(--accent); color: var(--text-strong); }
.perf-cell {
- position: relative; background: #0d0d14; border: 2px solid #24242e;
- border-radius: 10px; color: #6a6a78; cursor: pointer;
+ position: relative; background: var(--bg2); border: 2px solid var(--line);
+ border-radius: 10px; color: var(--muted2); cursor: pointer;
font-size: 17px; padding: 10px; overflow: hidden;
display: flex; align-items: center; justify-content: center; text-align: center;
}
-.perf-cell.filled { color: #e8e8ec; background: color-mix(in srgb, var(--slot, #35507f) 18%, #0d0d14); border-color: color-mix(in srgb, var(--slot, #35507f) 55%, #24242e); }
+.perf-cell.filled { color: var(--text); background: color-mix(in srgb, var(--slot, var(--slot-default)) 18%, var(--bg2)); border-color: color-mix(in srgb, var(--slot, var(--slot-default)) 55%, var(--line)); }
/* Panelle aynı renk şeridi ve çalma ilerlemesi (#637) */
.perf-color { position: absolute; left: 0; top: 0; bottom: 0; width: 6px; }
-.perf-progwrap { position: absolute; left: 6px; right: 0; bottom: 0; height: 5px; background: rgba(255,255,255,.05); }
-.perf-prog { display: block; height: 100%; width: 0%; background: var(--slot, #2ea043); }
-.perf-cell.cursor { border-color: #7a7a90; }
+.perf-progwrap { position: absolute; left: 6px; right: 0; bottom: 0; height: 5px; background: rgb(var(--fg-rgb) / .05); }
+.perf-prog { display: block; height: 100%; width: 0%; background: var(--slot, var(--cue-live)); }
+.perf-cell.cursor { border-color: var(--muted2); }
/* Hazırlanmış: ateşlenmeyi bekliyor, köşede geri sayım. */
-.perf-cell.armed { border-color: #f0b429; background: #2a2210; }
-.perf-cell.active { border-color: #2ea043; background: #10301c; color: #fff; }
+.perf-cell.armed { border-color: var(--cue-armed); background: color-mix(in srgb, var(--cue-armed) 14%, var(--bg2)); }
+.perf-cell.active { border-color: var(--cue-live); background: color-mix(in srgb, var(--cue-live) 22%, var(--bg2)); color: var(--text-strong); }
.perf-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.perf-count {
- position: absolute; right: 8px; bottom: 4px; font-size: 13px; color: #f0b429;
+ position: absolute; right: 8px; bottom: 4px; font-size: 13px; color: var(--cue-armed);
font-variant-numeric: tabular-nums;
}
-.perf-hint { color: #56566a; font-size: 12px; text-align: center; }
+.perf-hint { color: var(--muted2); font-size: 12px; text-align: center; }
/* Yuva önizlemesi: sahnenin renkleri, hücrenin arkasında. Metnin okunması
önce gelir, o yüzden önizleme soluk ve metin üstünde kalır. */
@@ -1385,7 +1447,7 @@ input[type=color].tl-swatch.big { width: 34px; height: 24px; }
.perf-cell.filled .perf-thumb { opacity: .32; }
/* Çakışma uyarısı — engellemiyoruz ama görünür olmalı. */
-.settings-io-note.warn { color: #f0b429; border-left: 2px solid #f0b429; padding-left: 8px; }
+.settings-io-note.warn { color: var(--warn-text); border-left: 2px solid var(--warn); padding-left: 8px; }
/* Kaynağı seçilmemiş yuva: dolu görünüyor ama ateşlenemez. Kesikli kenar
ve soluk metin bunu ateşlemeden önce belli eder. */
@@ -1395,12 +1457,12 @@ input[type=color].tl-swatch.big { width: 34px; height: 24px; }
/* OpenRGB aygıt listesi. Sürülemeyen aygıtlar sönük çizilir: renk kabul
etmeyen bir aygıta anahtar sunmak, çalışmayan bir düğme sunmaktır. */
.orgb-devices { display: flex; flex-direction: column; gap: 4px; padding: 4px 0; }
-.orgb-device { display: flex; align-items: center; gap: 10px; padding: 4px 8px; border-radius: 6px; background: rgba(255,255,255,.03); }
+.orgb-device { display: flex; align-items: center; gap: 10px; padding: 4px 8px; border-radius: 6px; background: rgb(var(--fg-rgb) / .03); }
.orgb-device.dim { opacity: .5; }
.orgb-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.orgb-meta { flex: 0 0 auto; opacity: .7; font-variant-numeric: tabular-nums; }
.orgb-warn { flex: 0 0 auto; font-size: 11px; opacity: .8; }
-.link-note { display: inline-block; padding: 2px 0 8px; font-size: 12px; opacity: .85; }
+.link-note { display: inline-block; padding: 2px 0 8px; font-size: 12px; }
/* Güncellemeler kartı (#640) */
.upd-ver { font-weight: 700; font-variant-numeric: tabular-nums; }
@@ -1432,7 +1494,7 @@ input[type=color].tl-swatch.big { width: 34px; height: 24px; }
.mcp-steps { margin: 0 0 8px 18px; color: var(--muted); font-size: 12px; line-height: 1.45; }
.mcp-snippet {
margin: 0 0 8px; padding: 10px; border-radius: 8px;
- border: 1px solid var(--line); background: #12080b; color: #f3d0d4;
+ border: 1px solid var(--line); background: var(--code-bg); color: var(--code-fg);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 11px; line-height: 1.45; white-space: pre-wrap; word-break: break-all;
}
@@ -1442,7 +1504,7 @@ input[type=color].tl-swatch.big { width: 34px; height: 24px; }
text-align: left; border: 1px solid var(--line); background: var(--card2);
color: inherit; border-radius: 9px; padding: 8px 10px; cursor: pointer;
}
-.mcp-mode.on { border-color: #c4364a; }
+.mcp-mode.on { border-color: var(--accent); }
.mcp-mode-name { display: block; font-weight: 700; font-size: 12px; }
.mcp-mode-hint { display: block; color: var(--muted); font-size: 11px; margin-top: 2px; }
@@ -1519,10 +1581,10 @@ textarea.p-in.lyr-editor-text,
textarea.p-in.lyr-editor-text {
grid-area: 1 / 1;
color: transparent;
- caret-color: #fff;
+ caret-color: var(--text-strong);
}
textarea.p-in.lyr-editor-text::selection {
- background: rgba(225, 29, 42, .35);
+ background: rgb(var(--accent-rgb) / .35);
color: transparent;
}
textarea.p-in.lyr-editor-text.lyr-plain {
@@ -1569,5 +1631,37 @@ input[type="number"]::-webkit-inner-spin-button {
.export-queue-row .q-status { font-size: 12px; white-space: nowrap; }
.export-queue-row .q-actions { display: flex; gap: 4px; }
.export-queue-row.q-running { border-color: var(--accent); }
-.export-queue-row.q-done .q-status { color: var(--ok, #4caf50); }
-.export-queue-row.q-error .q-status { color: var(--err, #ff6b6b); }
+.export-queue-row.q-done .q-status { color: var(--ok); }
+.export-queue-row.q-error .q-status { color: var(--danger-text); }
+
+/* ============================ ARAYÜZ TEMASI (#770) ============================ */
+/* Her kart temanın kendi renkleriyle küçük bir panel çizer; renkler satır içi
+ olarak ui-themes.js tablosundan gelir. */
+.ui-theme-grid {
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); gap: 10px;
+}
+.ui-theme-opt {
+ display: flex; flex-direction: column; gap: 7px; padding: 7px; cursor: pointer;
+ background: var(--card2); border: 1px solid var(--line); border-radius: var(--radius-sm);
+ color: var(--muted); font: inherit; font-size: 11.5px; font-weight: 600; text-align: left;
+ transition: border-color .14s, color .14s;
+}
+.ui-theme-opt:hover { color: var(--text); border-color: var(--line2); }
+.ui-theme-opt.active { color: var(--text-strong); border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
+.ui-theme-opt:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
+.ui-theme-name { padding: 0 2px; }
+.ui-theme-preview {
+ display: flex; align-items: flex-end; height: 54px; padding: 8px; flex: 1;
+ border: 1px solid; border-radius: 7px; overflow: hidden;
+}
+.ui-theme-card {
+ display: flex; flex-direction: column; gap: 4px; width: 100%; padding: 6px;
+ border: 1px solid; border-radius: 5px;
+}
+.ui-theme-line { display: block; height: 4px; width: 80%; border-radius: 2px; }
+.ui-theme-line.short { width: 52%; }
+.ui-theme-btn { display: block; height: 7px; width: 40%; border-radius: 3px; margin-top: 2px; }
+.ui-theme-split { display: flex; height: 54px; flex: 1; border-radius: 7px; overflow: hidden; }
+.ui-theme-split .ui-theme-preview { border-radius: 0; height: 100%; min-width: 0; padding: 8px 5px; }
+.ui-theme-split .ui-theme-preview:first-child { border-radius: 7px 0 0 7px; }
+.ui-theme-split .ui-theme-preview + .ui-theme-preview { border-left: 0; border-radius: 0 7px 7px 0; }
diff --git a/src/admin/admin.js b/src/admin/admin.js
index 500e0b32..f2689a88 100644
--- a/src/admin/admin.js
+++ b/src/admin/admin.js
@@ -662,6 +662,8 @@
return window.SVUpdatesPanel ? window.SVUpdatesPanel.panel() : null;
case 'language':
return languageCtrl();
+ case 'uitheme':
+ return uiThemeCtrl();
case 'extendedrange':
return extendedRangeCtrl();
case 'nowplayingpanel':
@@ -1015,6 +1017,94 @@
]);
}
+ // --- Arayüz teması (#770) ---
+ /* Kartlar temanın kendi renkleriyle küçük bir panel çizer: zemin, kart,
+ yazı, vurgu. Renkler src/shared/ui-themes.js tablosundan; bu dosyada
+ renk yazılmaz. Seçim hemen uygulanır ve ayar dosyasına gider. */
+ function uiThemeCtrl() {
+ const UT = window.SVUiThemes;
+ if (!UT || !window.SVUiTheme) return null;
+ const current = () => UT.normalizeId(cfg.ui && cfg.ui.theme);
+ const prefersDark = () => !(window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches);
+ const opts = [];
+ const pick = (id) => {
+ if (!cfg.ui || typeof cfg.ui !== 'object') cfg.ui = {};
+ cfg.ui.theme = id;
+ window.SVUiTheme.set(id);
+ opts.forEach((o) => {
+ const on = o.dataset.theme === id;
+ o.classList.toggle('active', on);
+ o.setAttribute('aria-checked', on ? 'true' : 'false');
+ o.tabIndex = on ? 0 : -1;
+ });
+ push(true);
+ scheduleRender();
+ };
+ const preview = (t) => {
+ const k = t.tokens;
+ const box = el('span', { class: 'ui-theme-preview', 'aria-hidden': 'true' });
+ box.style.background = k['--bg'];
+ box.style.borderColor = k['--line2'];
+ const card = el('span', { class: 'ui-theme-card' });
+ card.style.background = k['--card'];
+ card.style.borderColor = k['--line'];
+ const l1 = el('span', { class: 'ui-theme-line' });
+ l1.style.background = k['--text'];
+ const l2 = el('span', { class: 'ui-theme-line short' });
+ l2.style.background = k['--muted2'];
+ const btn = el('span', { class: 'ui-theme-btn' });
+ btn.style.background = 'linear-gradient(135deg, ' + k['--accent'] + ', ' + k['--accent-grad'] + ')';
+ card.append(l1, l2, btn);
+ box.appendChild(card);
+ return box;
+ };
+ const grid = el('div', { class: 'ui-theme-grid', role: 'radiogroup', 'aria-label': tr('Arayüz Teması') });
+ UT.CHOICES.forEach((c) => {
+ const isSystem = c.id === UT.SYSTEM_ID;
+ let swatch;
+ if (isSystem) {
+ // Yarısı Karanlık, yarısı Aydınlık
+ swatch = el('span', { class: 'ui-theme-split', 'aria-hidden': 'true' }, [
+ preview(UT.byId('dark')), preview(UT.byId('light')),
+ ]);
+ } else {
+ swatch = preview(UT.byId(c.id));
+ }
+ const label = el('span', { class: 'ui-theme-name', text: tr(c.name) });
+ const o = el('button', {
+ type: 'button', class: 'ui-theme-opt', role: 'radio',
+ 'data-theme': c.id, onclick: () => pick(c.id),
+ }, [swatch, label]);
+ opts.push(o);
+ grid.appendChild(o);
+ });
+ // Ok tuşlarıyla gezinme (radyo grubu davranışı)
+ grid.addEventListener('keydown', (e) => {
+ const i = opts.findIndex((o) => o.dataset.theme === current());
+ let j = -1;
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') j = (i + 1) % opts.length;
+ else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') j = (i - 1 + opts.length) % opts.length;
+ if (j < 0) return;
+ e.preventDefault();
+ pick(opts[j].dataset.theme);
+ opts[j].focus();
+ });
+ const sel = current();
+ opts.forEach((o) => {
+ const on = o.dataset.theme === sel;
+ o.classList.toggle('active', on);
+ o.setAttribute('aria-checked', on ? 'true' : 'false');
+ o.tabIndex = on ? 0 : -1;
+ });
+ const sysNote = sel === UT.SYSTEM_ID
+ ? tr('Cihazınız şu an {0} kullanıyor; değişince panel de değişir.').replace('{0}', tr(prefersDark() ? 'Karanlık' : 'Aydınlık'))
+ : tr('Yalnız paneli değiştirir; görselleştirici çıktısı, yayın katmanı ve kendi renkleriniz etkilenmez.');
+ return el('div', { class: 'ctrl' }, [
+ grid,
+ el('div', { class: 'studio-note dim-hint', text: sysNote, style: 'margin-top:6px;' }),
+ ]);
+ }
+
// --- Genişletilmiş aralıklar (localStorage; cfg yolu yok) ---
function extendedRangeCtrl() {
const input = el('input', {
@@ -2478,7 +2568,7 @@
},
{
id: 'settings', icon: 'gear', title: 'Ayarlar',
- desc: 'Dil, pencere koruması, genişletilmiş aralıklar ve güncellemeler.',
+ desc: 'Dil, arayüz teması, pencere koruması, genişletilmiş aralıklar ve güncellemeler.',
},
];
@@ -3459,9 +3549,10 @@
icon: 'gear',
title: 'Uygulama',
desc: '',
- roots: ['power.alwaysOnTop', 'power.keepAwake', 'power.hwVideoDecode', 'power.protect', 'power.protectNoEscape', 'power.confirmClose'],
+ roots: ['ui.theme', 'power.alwaysOnTop', 'power.keepAwake', 'power.hwVideoDecode', 'power.protect', 'power.protectNoEscape', 'power.confirmClose'],
controls: [
{ type: 'language', group: 'Dil' },
+ { type: 'uitheme', group: 'Arayüz Teması' },
{
type: 'toggle', path: 'power.alwaysOnTop', label: 'Görselleştirmeyi Her Zaman Üstte Tut',
group: 'Pencere',
@@ -4245,8 +4336,22 @@
if (renderWanted) scheduleRender();
}
+ /* Ayardaki tema her çizimde uygulanır: MCP, yedekten dönüş ve sıfırlama
+ cfg'yi değiştirip render() çağırıyor (#770). */
+ function syncUiTheme() {
+ if (!window.SVUiTheme || !window.SVUiThemes) return;
+ const id = window.SVUiThemes.normalizeId(cfg && cfg.ui && cfg.ui.theme);
+ if (window.SVUiTheme.choice() !== id) window.SVUiTheme.set(id);
+ }
+ /* "Cihaz varsayılanı" seçiliyken sistem açık/koyu değişince Ayarlar'daki
+ "cihazınız şu an …" notu da güncellensin. */
+ if (window.SVUiTheme) {
+ window.SVUiTheme.onChange(() => { if (activeCategory === 'settings') scheduleRender(); });
+ }
+
function render() {
renderWanted = false;
+ syncUiTheme();
const root = $('sections');
const paneScroll = captureLayerPaneScroll(root);
/* Kategori değişirken eski sayfanın kutusu yeni sayfada aranmaz: aynı
@@ -6286,11 +6391,14 @@
// Kullanıcı içeriği (renk şablonları ve sahneler) sıfırlamada korunur
const presets = Array.isArray(cfg.userPresets) ? cfg.userPresets.slice() : [];
const scenes = Array.isArray(cfg.scenes) ? cfg.scenes.slice() : [];
+ // Okunabilirlik için seçilmiş tema sıfırlamada kalır (#770)
+ const uiTheme = cfg.ui && cfg.ui.theme;
cfg = window.SV.defaultConfig();
platformAdjust(cfg);
cfg.audio.sources = sources;
cfg.userPresets = presets;
cfg.scenes = scenes;
+ if (uiTheme) cfg.ui.theme = uiTheme;
activeSceneId = null;
push(true);
render();
diff --git a/src/admin/index.html b/src/admin/index.html
index 661949c9..1d2217fc 100644
--- a/src/admin/index.html
+++ b/src/admin/index.html
@@ -12,6 +12,9 @@
Ses Görselleştirici — Yönetici Paneli
+
+
+
diff --git a/src/admin/mapping-panel.js b/src/admin/mapping-panel.js
index 42bab9d1..19022c94 100644
--- a/src/admin/mapping-panel.js
+++ b/src/admin/mapping-panel.js
@@ -103,14 +103,18 @@
function draw() {
const c = cv.getContext('2d');
+ // Renkler arayüz temasından (#770)
+ const UT = window.SVUiTheme;
+ const tint = (name, a) => (UT ? UT.alpha(name, a) : 'transparent');
+ const solid = (name) => (UT ? UT.color(name) : '');
c.clearRect(0, 0, W, H);
// Zemin
- c.fillStyle = 'rgba(255,255,255,0.03)';
+ c.fillStyle = tint('--fg-rgb', 0.03);
c.fillRect(0, 0, W, H);
// Referans dikdörtgeni (haritalanmamış görüntü)
const a = toPx(0, 0);
const b = toPx(1, 1);
- c.strokeStyle = 'rgba(255,255,255,0.16)';
+ c.strokeStyle = tint('--fg-rgb', 0.16);
c.setLineDash([4, 4]);
c.lineWidth = 1;
c.strokeRect(a[0], a[1], b[0] - a[0], b[1] - a[1]);
@@ -124,16 +128,16 @@
if (i === 0) c.moveTo(q[0], q[1]); else c.lineTo(q[0], q[1]);
});
c.closePath();
- c.fillStyle = 'rgba(124,92,255,0.12)';
+ c.fillStyle = tint('--sel-rgb', 0.12);
c.fill();
- c.strokeStyle = 'rgba(124,92,255,0.85)';
+ c.strokeStyle = tint('--sel-rgb', 0.85);
c.lineWidth = 1.5;
c.stroke();
// Bükme ızgarası
if (editMode === 'mesh') {
const g = meshFromPlain(out.mesh) || window.SVWarp.identityGrid(3, 3);
- c.strokeStyle = 'rgba(33,212,253,0.4)';
+ c.strokeStyle = tint('--info-rgb', 0.4);
c.lineWidth = 1;
const STEPS = 12;
for (let j = 0; j < g.rows; j++) {
@@ -165,9 +169,9 @@
if (i === 0) c.moveTo(q[0], q[1]); else c.lineTo(q[0], q[1]);
});
c.closePath();
- c.fillStyle = mi === activeMask && editMode === 'mask' ? 'rgba(255,80,80,0.28)' : 'rgba(0,0,0,0.45)';
+ c.fillStyle = mi === activeMask && editMode === 'mask' ? tint('--danger-rgb', 0.28) : tint('--shadow-rgb', 0.45);
c.fill();
- c.strokeStyle = 'rgba(255,120,120,0.8)';
+ c.strokeStyle = tint('--danger-rgb', 0.8);
c.stroke();
});
@@ -177,13 +181,13 @@
const on = hover === k || (drag && drag.k === k);
c.beginPath();
c.arc(q[0], q[1], on ? 7 : 5, 0, Math.PI * 2);
- c.fillStyle = on ? '#fff' : (pt.kind === 'mask' ? '#ff7070' : '#7c5cff');
+ c.fillStyle = on ? solid('--text-strong') : (pt.kind === 'mask' ? solid('--danger-text') : tint('--sel-rgb', 1));
c.fill();
- c.strokeStyle = 'rgba(0,0,0,0.6)';
+ c.strokeStyle = tint('--shadow-rgb', 0.6);
c.lineWidth = 1;
c.stroke();
if (pt.kind === 'corner') {
- c.fillStyle = 'rgba(255,255,255,0.6)';
+ c.fillStyle = tint('--fg-rgb', 0.6);
c.font = '10px system-ui, sans-serif';
c.textAlign = 'center';
c.fillText(String(pt.i + 1), q[0], q[1] - 10);
@@ -272,6 +276,13 @@
});
draw();
+ // Tema değişince yeniden çiz; panel yeniden kurulunca eski tuval bırakılır (#770)
+ if (window.SVUiTheme) {
+ const off = window.SVUiTheme.onChange(() => {
+ if (!cv.isConnected) { off(); return; }
+ draw();
+ });
+ }
return { node: box, redraw: draw };
}
diff --git a/src/admin/openrgb-panel.js b/src/admin/openrgb-panel.js
index 52c1d9a1..234e943e 100644
--- a/src/admin/openrgb-panel.js
+++ b/src/admin/openrgb-panel.js
@@ -63,11 +63,11 @@
if (state.connected) {
st.appendChild(el('span', { icon: 'check' }));
st.appendChild(el('span', { text: 'Bağlı' }));
- st.appendChild(el('span', { class: 'st-sep', text: ' · ' }));
+ st.appendChild(el('span', { class: 'st-sep', 'aria-hidden': 'true', text: ' · ' }));
st.appendChild(el('span', { class: 'st-num', text: String(state.drivable || 0) + ' ' }));
st.appendChild(el('span', { text: 'sürülebilir aygıt' }));
if (state.devices && state.devices.length > (state.drivable || 0)) {
- st.appendChild(el('span', { class: 'st-sep', text: ' · ' }));
+ st.appendChild(el('span', { class: 'st-sep', 'aria-hidden': 'true', text: ' · ' }));
st.appendChild(el('span', { class: 'st-num', text: String(state.devices.length - (state.drivable || 0)) + ' ' }));
st.appendChild(el('span', { text: 'yalnız kendi efektini oynatıyor' }));
}
diff --git a/src/admin/scene-panels.js b/src/admin/scene-panels.js
index 163c1005..5ae18da9 100644
--- a/src/admin/scene-panels.js
+++ b/src/admin/scene-panels.js
@@ -2391,10 +2391,10 @@
if (artnetState.running) {
st.appendChild(el('span', { icon: 'check' }));
st.appendChild(el('span', { text: 'Yayında' }));
- st.appendChild(el('span', { class: 'st-sep', text: ' · ' }));
+ st.appendChild(el('span', { class: 'st-sep', 'aria-hidden': 'true', text: ' · ' }));
st.appendChild(el('span', { text: 'Evren' }));
st.appendChild(el('span', { class: 'st-num', text: ' ' + artnetState.universe }));
- st.appendChild(el('span', { class: 'st-sep', text: ' · ' }));
+ st.appendChild(el('span', { class: 'st-sep', 'aria-hidden': 'true', text: ' · ' }));
st.appendChild(el('span', { class: 'st-num', text: String(artnetState.packets || 0) + ' ' }));
st.appendChild(el('span', { text: 'paket' }));
} else if (artnetState.error) {
diff --git a/src/admin/stream.js b/src/admin/stream.js
index 9ac7f375..71833ab9 100644
--- a/src/admin/stream.js
+++ b/src/admin/stream.js
@@ -105,10 +105,10 @@
if (status.running) {
statusBox.appendChild(el('span', { icon: 'check' }));
statusBox.appendChild(el('span', { text: status.lan ? 'Ağa açık' : 'Yalnızca bu bilgisayar' }));
- statusBox.appendChild(el('span', { class: 'st-sep', text: ' · ' }));
+ statusBox.appendChild(el('span', { class: 'st-sep', 'aria-hidden': 'true', text: ' · ' }));
statusBox.appendChild(el('span', { text: 'Port' }));
statusBox.appendChild(el('span', { class: 'st-num', text: ' ' + status.port }));
- statusBox.appendChild(el('span', { class: 'st-sep', text: ' · ' }));
+ statusBox.appendChild(el('span', { class: 'st-sep', 'aria-hidden': 'true', text: ' · ' }));
if (status.clients.length) {
statusBox.appendChild(el('span', { class: 'st-num', text: status.clients.length + ' ' }));
statusBox.appendChild(el('span', { text: 'bağlı istemci' }));
diff --git a/src/admin/studio.css b/src/admin/studio.css
index 67a65729..7937754c 100644
--- a/src/admin/studio.css
+++ b/src/admin/studio.css
@@ -58,14 +58,16 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
}
.studio-item:hover { color: var(--text); border-color: var(--line2); }
.studio-item.active {
- background: linear-gradient(135deg, var(--accent), var(--accent2));
- color: #fff; border-color: transparent;
+ background: linear-gradient(135deg, var(--accent), var(--accent-grad));
+ color: var(--on-accent); border-color: transparent;
}
.si-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.si-tag {
- font-size: 8.5px; text-transform: uppercase; letter-spacing: .06em; opacity: .7;
+ /* Saydamlık yerine ikincil yazı rengi: %70 saydamlıkta 3.7:1'e iniyordu (#770) */
+ font-size: 8.5px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted2);
border: 1px solid currentColor; border-radius: 4px; padding: 1px 4px; flex: none;
}
+.studio-item.active .si-tag { color: inherit; }
.studio-main { min-width: 0; }
.studio-editor { display: flex; flex-direction: column; gap: 9px; }
@@ -82,28 +84,28 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
background: var(--card2); border: 1px solid var(--line); color: var(--muted);
white-space: pre-wrap; word-break: break-word;
}
-.studio-status.ok { color: var(--ok); border-color: rgba(67, 217, 138, .28); }
-.studio-status.err { color: #ff8a8a; border-color: rgba(255, 80, 80, .3); background: rgba(60, 12, 18, .35); }
+.studio-status.ok { color: var(--ok); border-color: rgb(var(--ok-rgb) / .28); }
+.studio-status.err { color: var(--danger-text); border-color: rgb(var(--danger-rgb) / .3); background: rgb(var(--danger-rgb) / .1); }
.studio-note {
font-size: 11px; color: var(--muted); line-height: 1.55;
background: var(--bg2); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px;
}
.studio-note.media-fault {
- color: #ffb4b4;
- border-color: rgba(255, 80, 80, .35);
- background: rgba(60, 12, 18, .35);
+ color: var(--danger-text);
+ border-color: rgb(var(--danger-rgb) / .35);
+ background: rgb(var(--danger-rgb) / .1);
}
.studio-preview {
position: relative; width: 100%; aspect-ratio: 16 / 9; border-radius: 9px;
- overflow: hidden; background: #05040a; border: 1px solid var(--line2);
+ overflow: hidden; background: var(--media-bg); border: 1px solid var(--line2);
}
.studio-preview-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.sp-badge {
position: absolute; right: 6px; bottom: 5px; font-size: 8.5px; font-weight: 700;
letter-spacing: .06em; text-transform: uppercase;
- padding: 2px 5px; border-radius: 4px; background: rgba(0,0,0,.55); color: rgba(255,255,255,.5);
+ padding: 2px 5px; border-radius: 4px; background: var(--media-scrim); color: rgb(var(--on-media-rgb) / .5);
}
.studio-params { display: flex; flex-direction: column; gap: 5px; }
@@ -116,13 +118,13 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
/* --- kod editörü --- */
.code-editor {
- display: flex; background: #0d0a11; border: 1px solid var(--line);
+ display: flex; background: var(--code-bg); border: 1px solid var(--line);
border-radius: 9px; overflow: hidden; height: 420px;
}
.code-gutter {
flex: none; width: 42px; padding: 10px 6px 10px 0; text-align: right;
font: 12px/1.5 'Cascadia Code', 'Consolas', 'SF Mono', monospace;
- color: var(--muted2); background: #0a0810; border-right: 1px solid var(--line);
+ color: var(--muted2); background: var(--code-gutter); border-right: 1px solid var(--line);
overflow: hidden; white-space: pre; user-select: none;
}
.code-body { position: relative; flex: 1; min-width: 0; }
@@ -132,22 +134,22 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
white-space: pre; overflow: auto; tab-size: 2;
border: 0; outline: none;
}
-.code-hl { pointer-events: none; color: #e7dfe9; }
+.code-hl { pointer-events: none; color: var(--code-fg); }
.code-input {
background: transparent; color: transparent; caret-color: var(--accent2);
resize: none; user-select: text;
}
-.code-input::selection { background: rgba(225, 29, 42, .3); }
+.code-input::selection { background: rgb(var(--accent-rgb) / .3); }
.code-errline {
position: absolute; left: 0; right: 0; display: none; pointer-events: none;
- background: rgba(255, 60, 60, .14); border-left: 2px solid #ff5a5a;
-}
-.c-kw { color: #ff7b8a; }
-.c-typ { color: #7fd6ff; }
-.c-fn { color: #c39bff; }
-.c-num { color: #ffc46b; }
-.c-com { color: #6d5f72; font-style: italic; }
-.c-pre { color: #8fdca0; }
+ background: rgb(var(--danger-rgb) / .14); border-left: 2px solid var(--danger-text);
+}
+.c-kw { color: var(--syn-kw); }
+.c-typ { color: var(--syn-type); }
+.c-fn { color: var(--syn-fn); }
+.c-num { color: var(--syn-num); }
+.c-com { color: var(--syn-com); font-style: italic; }
+.c-pre { color: var(--syn-pre); }
/* ======================= KONTROL YÜZEYLERİ ======================= */
.control-panel { display: flex; flex-direction: column; gap: 8px; }
@@ -252,7 +254,7 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
.mded-tabs .layer-tab { display: inline-flex; align-items: center; gap: 5px; }
.mded-badge {
min-width: 16px; padding: 0 5px; border-radius: 8px; font-size: 10px; line-height: 16px; text-align: center;
- background: rgba(255, 90, 90, .22); color: #ffb3b8;
+ background: rgb(var(--danger-rgb) / .22); color: var(--danger-text);
}
.layer-tab.dim { opacity: .55; }
.mded-slots { display: flex; flex-wrap: wrap; gap: 5px; }
@@ -260,37 +262,38 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
.mded-field { display: flex; flex-direction: column; gap: 4px; }
.mded-label { font-size: 11px; color: var(--muted2); font-weight: 600; }
.mded-code {
- display: flex; background: #0d0a11; border: 1px solid var(--line); border-radius: 9px; overflow: hidden;
+ display: flex; background: var(--code-bg); border: 1px solid var(--line); border-radius: 9px; overflow: hidden;
}
.mded-gutter {
flex: none; width: 38px; padding: 8px 6px 8px 0; text-align: right; overflow: hidden;
font: 12px/1.5 'Cascadia Code', 'Consolas', 'SF Mono', monospace;
- color: var(--muted2); background: #0a0810; border-right: 1px solid var(--line); user-select: none;
+ color: var(--muted2); background: var(--code-gutter); border-right: 1px solid var(--line); user-select: none;
}
-.mded-gutter .err { color: #ff5a5a; font-weight: 700; background: rgba(255, 60, 60, .14); }
+.mded-gutter .err { color: var(--danger-text); font-weight: 700; background: rgb(var(--danger-rgb) / .14); }
.mded-input {
flex: 1; min-width: 0; margin: 0; padding: 8px 10px; resize: vertical; border: 0; outline: none;
- background: transparent; color: #e7dfe9; caret-color: var(--accent2);
+ background: transparent; color: var(--code-fg); caret-color: var(--accent2);
font: 12px/1.5 'Cascadia Code', 'Consolas', 'SF Mono', monospace; white-space: pre; overflow: auto; tab-size: 2;
}
-.mded-input::selection { background: rgba(225, 29, 42, .3); }
+.mded-input::selection { background: rgb(var(--accent-rgb) / .3); }
.mded-raw { min-height: 320px; }
.mded-diag { display: flex; flex-direction: column; gap: 4px; }
.mded-ok { font-size: 11.5px; color: var(--ok); }
.mded-err, .mded-over {
display: flex; flex-direction: column; gap: 2px; text-align: left; cursor: pointer;
padding: 6px 8px; border-radius: 8px; font-size: 11.5px; color: var(--text);
- background: rgba(255, 60, 60, .08); border: 1px solid rgba(255, 90, 90, .28);
+ background: rgb(var(--danger-rgb) / .08); border: 1px solid rgb(var(--danger-rgb) / .28);
}
-.mded-over { background: rgba(255, 190, 70, .07); border-color: rgba(255, 190, 70, .28); margin-top: 4px; }
-.mded-err:hover, .mded-over:hover { border-color: rgba(255, 120, 120, .6); }
+.mded-over { background: rgb(var(--warn-rgb) / .07); border-color: rgb(var(--warn-rgb) / .28); margin-top: 4px; }
+.mded-err:hover, .mded-over:hover { border-color: rgb(var(--danger-rgb) / .6); }
.mded-where { font-size: 10.5px; color: var(--muted2); }
.mded-msg { font-weight: 600; }
.mded-code-snip {
- font: 11px/1.4 'Cascadia Code', 'Consolas', monospace; color: #d9c9dc;
+ font: 11px/1.4 'Cascadia Code', 'Consolas', monospace; color: var(--code-fg);
white-space: pre-wrap; word-break: break-all; opacity: .85;
}
.st-num { font-variant-numeric: tabular-nums; }
+/* Süs ayracı: betikte aria-hidden, okunmaz ve karşıtlık ölçümüne girmez (#770) */
.st-sep { opacity: .5; }
/* ==================== KATMAN / EFEKT YIĞINI ==================== */
@@ -337,7 +340,7 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
.beat-dot.hit { background: var(--accent2); box-shadow: 0 0 12px var(--accent2); }
.bpm-value { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; min-width: 84px; }
.bpm-conf { flex: 1; height: 4px; background: var(--card2); border-radius: 3px; overflow: hidden; }
-.bpm-conf i { display: block; height: 100%; width: 0; background: linear-gradient(90deg, var(--accent), var(--accent2)); transition: width .25s; }
+.bpm-conf i { display: block; height: 100%; width: 0; background: linear-gradient(90deg, var(--accent), var(--accent-grad)); transition: width .25s; }
/* Otomatik VJ durum satırı: kullanıcı çalıştığını buradan görür. Eskiden
hiçbir geri bildirim yoktu ve özellik "çalışmıyor" sanılıyordu. */
@@ -386,8 +389,8 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
margin-top: 6px;
}
.mod-macro {
- background: var(--card2, rgba(255, 255, 255, 0.03));
- border: 1px solid var(--line, rgba(255, 255, 255, 0.08));
+ background: var(--card2);
+ border: 1px solid var(--line);
border-radius: 8px;
padding: 7px 8px;
}
@@ -401,8 +404,8 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
.mod-macro input[type='range'] { width: 100%; margin-top: 5px; }
.mod-gen {
- background: var(--card2, rgba(255, 255, 255, 0.03));
- border: 1px solid var(--line, rgba(255, 255, 255, 0.08));
+ background: var(--card2);
+ border: 1px solid var(--line);
border-radius: 8px;
padding: 8px 9px;
margin-bottom: 8px;
@@ -410,8 +413,8 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
.mod-routes { display: flex; flex-direction: column; gap: 8px; }
.mod-route {
- background: var(--card2, rgba(255, 255, 255, 0.03));
- border: 1px solid var(--line, rgba(255, 255, 255, 0.08));
+ background: var(--card2);
+ border: 1px solid var(--line);
border-radius: 9px;
padding: 8px 9px;
}
@@ -422,7 +425,7 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
height: 6px;
min-width: 60px;
border-radius: 4px;
- background: rgba(255, 255, 255, 0.08);
+ background: rgb(var(--fg-rgb) / .08);
overflow: hidden;
}
.mod-meter i {
@@ -430,7 +433,7 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
height: 100%;
width: 0;
border-radius: 4px;
- background: linear-gradient(90deg, var(--accent, #7c5cff), var(--accent2, #21d4fd));
+ background: linear-gradient(90deg, var(--accent), var(--accent-grad));
transition: width 80ms linear;
}
@@ -443,18 +446,18 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
justify-content: space-between;
gap: 12px;
padding: 8px 12px;
- background: var(--card2, rgba(255, 255, 255, 0.03));
- border: 1px solid var(--line, rgba(255, 255, 255, 0.08));
+ background: var(--card2);
+ border: 1px solid var(--line);
border-radius: 9px;
font-size: 11.5px;
}
.an-toolbar-title {
font-weight: 600;
- color: var(--fg, #f0f0f5);
+ color: var(--text);
}
.an-toolbar-desc {
font-size: 11px;
- color: var(--muted, rgba(255, 255, 255, 0.55));
+ color: var(--muted);
margin-top: 1px;
}
.an-banner {
@@ -463,16 +466,16 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
justify-content: space-between;
gap: 12px;
padding: 9px 12px;
- background: rgba(255, 170, 0, 0.12);
- border: 1px solid rgba(255, 170, 0, 0.35);
+ background: rgb(var(--warn-rgb) / .12);
+ border: 1px solid rgb(var(--warn-rgb) / .35);
border-radius: 8px;
- color: #ffca66;
+ color: var(--warn-text);
font-size: 11.5px;
line-height: 1.4;
}
.an-banner-btn {
- background: #ffa800;
- color: #111;
+ background: var(--warn);
+ color: var(--on-warn);
border: none;
border-radius: 6px;
padding: 4px 10px;
@@ -492,8 +495,8 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
gap: 10px;
}
.an-col {
- background: var(--card2, rgba(255, 255, 255, 0.03));
- border: 1px solid var(--line, rgba(255, 255, 255, 0.08));
+ background: var(--card2);
+ border: 1px solid var(--line);
border-radius: 9px;
padding: 9px 10px;
}
@@ -503,7 +506,7 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
- color: var(--muted2, rgba(255, 255, 255, 0.45));
+ color: var(--muted2);
}
.an-row {
display: flex;
@@ -512,7 +515,7 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
min-height: 20px;
font-size: 11.5px;
}
-.an-lbl { color: var(--muted, rgba(255, 255, 255, 0.65)); white-space: nowrap; }
+.an-lbl { color: var(--muted); white-space: nowrap; }
.an-val { margin-left: auto; font-variant-numeric: tabular-nums; font-weight: 600; }
.an-bar {
margin-left: auto;
@@ -520,7 +523,7 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
max-width: 96px;
height: 6px;
border-radius: 4px;
- background: rgba(255, 255, 255, 0.08);
+ background: rgb(var(--fg-rgb) / .08);
overflow: hidden;
}
.an-bar i {
@@ -528,7 +531,7 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
height: 100%;
width: 0;
border-radius: 4px;
- background: linear-gradient(90deg, var(--accent, #7c5cff), var(--accent2, #21d4fd));
+ background: linear-gradient(90deg, var(--accent), var(--accent-grad));
transition: width 100ms linear;
}
@@ -539,12 +542,12 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
margin-top: 6px;
}
.an-note { display: flex; flex-direction: column; align-items: center; gap: 4px; }
-.an-note-name { font-size: 10px; color: var(--muted2, rgba(255, 255, 255, 0.45)); }
+.an-note-name { font-size: 10px; color: var(--muted2); }
.an-note-bar {
width: 100%;
height: 48px;
border-radius: 4px;
- background: rgba(255, 255, 255, 0.06);
+ background: rgb(var(--fg-rgb) / .06);
display: flex;
align-items: flex-end;
overflow: hidden;
@@ -556,7 +559,7 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
width: 100%;
height: 0;
border-radius: 3px;
- background: linear-gradient(180deg, var(--accent2, #21d4fd), var(--accent, #7c5cff));
+ background: linear-gradient(180deg, var(--accent-grad), var(--accent));
transition: height 100ms linear;
}
@@ -564,8 +567,8 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
/* ===== Sahne geçişi paneli ===== */
.tr-panel { display: flex; flex-direction: column; gap: 8px; }
.tr-params {
- background: var(--card2, rgba(255, 255, 255, 0.03));
- border: 1px solid var(--line, rgba(255, 255, 255, 0.08));
+ background: var(--card2);
+ border: 1px solid var(--line);
border-radius: 9px;
padding: 8px 9px;
}
@@ -574,8 +577,8 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
/* ===== Projeksiyon haritalama ===== */
.map-panel { display: flex; flex-direction: column; gap: 9px; }
.map-edit {
- background: rgba(0, 0, 0, 0.35);
- border: 1px solid var(--line, rgba(255, 255, 255, 0.08));
+ background: rgb(var(--shade-rgb) / .35);
+ border: 1px solid var(--line);
border-radius: 10px;
padding: 6px;
overflow: hidden;
@@ -589,21 +592,21 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
touch-action: none;
outline: none;
}
-.map-canvas:focus { box-shadow: 0 0 0 2px var(--accent, #7c5cff) inset; }
+.map-canvas:focus { box-shadow: 0 0 0 2px var(--accent) inset; }
.map-nums { display: flex; flex-direction: column; gap: 5px; }
.map-num { display: flex; align-items: center; gap: 6px; }
.map-num-lbl {
flex: 0 0 70px;
font-size: 11px;
- color: var(--muted, rgba(255, 255, 255, 0.65));
+ color: var(--muted);
}
.map-num input { flex: 1; min-width: 0; }
.map-masks { display: flex; flex-direction: column; gap: 4px; }
.btn.tiny.active {
- background: var(--accent, #7c5cff);
- color: #fff;
+ background: var(--accent);
+ color: var(--on-accent);
border-color: transparent;
}
@@ -611,8 +614,8 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
/* ===== MilkDrop paneli ===== */
.md-panel { display: flex; flex-direction: column; gap: 8px; }
.md-cur { margin-left: auto; font-weight: 600; font-size: 12px; }
-.md-ok { margin-left: auto; color: var(--ok, #4ade80); font-size: 12px; }
-.md-err { margin-left: auto; color: var(--warn, #fbbf24); font-size: 12px; }
+.md-ok { margin-left: auto; color: var(--ok); font-size: 12px; }
+.md-err { margin-left: auto; color: var(--warn); font-size: 12px; }
.md-errbox { white-space: pre-wrap; font-family: ui-monospace, monospace; font-size: 11px; }
.md-list {
display: flex;
@@ -620,7 +623,7 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
gap: 2px;
max-height: 300px;
overflow-y: auto;
- border: 1px solid var(--line, rgba(255, 255, 255, 0.08));
+ border: 1px solid var(--line);
border-radius: 8px;
padding: 4px;
}
@@ -628,7 +631,7 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
/* Pencereli listenin boşluk kutuları (#635): içleri boş, esnek kutuda
küçülmemeli; ızgarada bütün sütunları kaplamalı. */
.md-spacer { flex: none; grid-column: 1 / -1; }
-.md-item.active { background: rgba(124, 92, 255, 0.18); }
+.md-item.active { background: rgb(var(--sel-rgb) / .18); }
/* Küçük resimli ızgara (#575): listeyle aynı çerçeve. Hücre 16:9 küçük
resim + ad; yıldız ve silme köşede. Bekleyen hücre çizgili, çizilemeyen
kesikli çerçeveli — ikisi de siyah bir küçük resimden ayrı görünmeli:
@@ -639,7 +642,7 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
gap: 6px;
max-height: 420px;
overflow-y: auto;
- border: 1px solid var(--line, rgba(255, 255, 255, 0.08));
+ border: 1px solid var(--line);
border-radius: 8px;
padding: 6px;
}
@@ -658,10 +661,10 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
text-align: left;
cursor: pointer;
}
-.md-cell-main:hover { background: rgba(255, 255, 255, 0.05); }
+.md-cell-main:hover { background: rgb(var(--fg-rgb) / .05); }
.md-cell.active .md-cell-main {
- background: rgba(124, 92, 255, 0.18);
- border-color: rgba(124, 92, 255, 0.55);
+ background: rgb(var(--sel-rgb) / .18);
+ border-color: rgb(var(--sel-rgb) / .55);
}
.md-thumb-box {
position: relative;
@@ -669,13 +672,13 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
aspect-ratio: 16 / 9;
overflow: hidden;
border-radius: 5px;
- background: #000;
+ background: var(--media-bg);
}
.md-thumb-box img { display: block; width: 100%; height: 100%; object-fit: cover; }
.md-thumb-box.pending img,
.md-thumb-box.failed img { display: none; }
.md-thumb-box.pending {
- background: repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0 6px, rgba(255, 255, 255, 0.02) 6px 12px);
+ background: repeating-linear-gradient(135deg, rgb(var(--fg-rgb) / .08) 0 6px, rgb(var(--fg-rgb) / .02) 6px 12px);
animation: md-thumb-wait 1.6s ease-in-out infinite;
}
@keyframes md-thumb-wait { 50% { opacity: 0.55; } }
@@ -683,19 +686,19 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
.md-thumb-box.pending { animation: none; }
}
.md-thumb-box.failed {
- background: rgba(255, 255, 255, 0.03);
- outline: 1px dashed rgba(255, 138, 138, 0.55);
+ background: rgb(var(--fg-rgb) / .03);
+ outline: 1px dashed rgb(var(--danger-rgb) / .55);
outline-offset: -1px;
}
.md-thumb-box.failed::after {
+ /* Çarpı maske: rengi temadan gelsin (#770) */
content: '';
- background: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22%3E%3Cpath d=%22M6.5 6.5l11 11%22 fill=%22none%22 stroke=%22rgba(255,138,138,.8)%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22/%3E%3Cpath d=%22M17.5 6.5l-11 11%22 fill=%22none%22 stroke=%22rgba(255,138,138,.8)%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22/%3E%3C/svg%3E") center / 22px no-repeat;
+ background: var(--danger-text);
+ opacity: .8;
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22%3E%3Cpath d=%22M6.5 6.5l11 11M17.5 6.5l-11 11%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.8%22 stroke-linecap=%22round%22/%3E%3C/svg%3E") center / 22px no-repeat;
+ mask: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22%3E%3Cpath d=%22M6.5 6.5l11 11M17.5 6.5l-11 11%22 fill=%22none%22 stroke=%22%23000%22 stroke-width=%221.8%22 stroke-linecap=%22round%22/%3E%3C/svg%3E") center / 22px no-repeat;
position: absolute;
inset: 0;
- display: grid;
- place-items: center;
- color: rgba(255, 138, 138, 0.8);
- font-size: 14px;
}
.md-cell-name {
font-size: 11px;
@@ -708,7 +711,7 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
top: 7px;
right: 7px;
padding: 2px 5px;
- background: rgba(0, 0, 0, 0.55);
+ background: var(--media-scrim);
border-radius: 999px;
}
.md-cell .md-cell-del {
@@ -728,7 +731,7 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
gap: 2px;
max-height: 220px;
overflow-y: auto;
- border: 1px solid var(--line, rgba(255, 255, 255, 0.08));
+ border: 1px solid var(--line);
border-radius: 8px;
padding: 4px;
}
@@ -740,8 +743,8 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
/* Kilitli preset: düğme basılı görünsün, yoksa kilidin açık mı kapalı mı
olduğu yalnız simgeden okunuyordu. */
.btn.md-locked {
- background: rgba(124, 92, 255, 0.22);
- border-color: rgba(124, 92, 255, 0.55);
+ background: rgb(var(--sel-rgb) / .22);
+ border-color: rgb(var(--sel-rgb) / .55);
}
/* Puan yıldızları: düğme görünümü değil, satır içi simgeler. 0 ayrı ve
soluk, çünkü "puansız" değil "rastgele hiç gelmesin" demek. */
@@ -753,19 +756,19 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
font-size: 15px;
line-height: 1;
cursor: pointer;
- color: var(--muted, #9aa);
+ color: var(--muted);
}
-.md-star.on { color: #ffc94d; }
+.md-star.on { color: var(--warn); }
/* Puan verilmemiş preset: yıldızlar sönük — "0 puan" değil, "puan yok". */
.md-stars.md-unrated .md-star { opacity: 0.55; }
.md-star0 {
font-size: 11px;
margin-right: 4px;
padding: 1px 5px;
- border: 1px solid rgba(255, 255, 255, 0.14);
+ border: 1px solid rgb(var(--fg-rgb) / .14);
border-radius: 999px;
}
-.md-star0.on { color: #ff8a8a; border-color: rgba(255, 138, 138, 0.5); }
+.md-star0.on { color: var(--danger-text); border-color: rgb(var(--danger-rgb) / .5); }
/* Yerleşik presetin silme düğmesi yerinde duran etiket: satır yüksekliği
silinebilen satırlarla aynı kalsın diye aynı yuvayı dolduruyor. */
.md-builtin {
@@ -774,8 +777,8 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
font-size: 10px;
letter-spacing: 0.04em;
text-transform: uppercase;
- color: var(--muted, #9aa);
- border: 1px solid rgba(255, 255, 255, 0.14);
+ color: var(--muted);
+ border: 1px solid rgb(var(--fg-rgb) / .14);
border-radius: 999px;
white-space: nowrap;
}
@@ -794,7 +797,7 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
text-overflow: ellipsis;
white-space: nowrap;
}
-.md-name:hover { background: rgba(255, 255, 255, 0.06); }
+.md-name:hover { background: rgb(var(--fg-rgb) / .06); }
/* Favori ve etiketler (#576). Listede yıldız satırın başında ve yıldızlar
gibi satır içi; etiketler adın yanında soluk ve kısalabiliyor — ad önce
okunsun. */
@@ -806,21 +809,21 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
font-size: 14px;
line-height: 1;
cursor: pointer;
- color: var(--muted, #9aa);
+ color: var(--muted);
}
-.md-fav.on { color: #ffc94d; }
+.md-fav.on { color: var(--warn); }
.md-tagline {
flex: 0 1 auto;
max-width: 40%;
font-size: 10px;
- color: var(--muted, #9aa);
+ color: var(--muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.btn.md-fav-live.on {
- color: #ffc94d;
- border-color: rgba(255, 201, 77, 0.5);
+ color: var(--warn);
+ border-color: rgb(var(--warn-rgb) / .5);
}
.md-tagsbox { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.md-tagsbox .md-tags-in { width: 100%; }
@@ -828,23 +831,23 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
.md-tagchip {
padding: 1px 7px;
font-size: 11px;
- color: var(--muted, #9aa);
+ color: var(--muted);
background: none;
- border: 1px solid rgba(255, 255, 255, 0.14);
+ border: 1px solid rgb(var(--fg-rgb) / .14);
border-radius: 999px;
cursor: pointer;
}
.md-tagchip.on {
color: inherit;
- background: rgba(124, 92, 255, 0.22);
- border-color: rgba(124, 92, 255, 0.55);
+ background: rgb(var(--sel-rgb) / .22);
+ border-color: rgb(var(--sel-rgb) / .55);
}
/* Makinede bulunan kütüphaneler (#574): ad, sayılar, içe aktar. Yol ipucu
olarak adın üstünde. */
.md-libs { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.md-lib { display: flex; align-items: center; gap: 8px; min-width: 0; }
.md-lib-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.md-lib-count { flex: none; font-size: 11px; color: var(--muted, #9aa); font-variant-numeric: tabular-nums; }
+.md-lib-count { flex: none; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
/* ===== Kayıt paneli ===== */
@@ -859,9 +862,9 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
}
.rec-status { word-break: break-all; }
.btn.danger {
- background: #e5484d;
+ background: var(--danger);
border-color: transparent;
- color: #fff;
+ color: var(--on-solid);
}
@@ -881,8 +884,8 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
flex-direction: column;
gap: 4px;
text-align: left;
- background: var(--card2, rgba(255, 255, 255, 0.03));
- border: 1px solid var(--line, rgba(255, 255, 255, 0.08));
+ background: var(--card2);
+ border: 1px solid var(--line);
border-radius: 10px;
padding: 8px 9px 9px;
color: inherit;
@@ -890,14 +893,14 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
cursor: pointer;
transition: border-color 120ms, background 120ms;
}
-.tpl-card:hover { background: rgba(255, 255, 255, 0.06); border-color: rgba(255, 255, 255, 0.18); }
-.tpl-card.active { border-color: var(--accent, #7c5cff); }
+.tpl-card:hover { background: rgb(var(--fg-rgb) / .06); border-color: rgb(var(--fg-rgb) / .18); }
+.tpl-card.active { border-color: var(--accent); }
.tpl-swatch { display: block; height: 26px; border-radius: 6px; }
.tpl-name { font-size: 13px; font-weight: 600; }
-.tpl-desc { font-size: 11px; color: var(--muted, rgba(255, 255, 255, 0.62)); line-height: 1.35; }
+.tpl-desc { font-size: 11px; color: var(--muted); line-height: 1.35; }
.tpl-group {
font-size: 10px;
- color: var(--muted2, rgba(255, 255, 255, 0.4));
+ color: var(--muted2);
text-transform: uppercase;
letter-spacing: 0.05em;
}
@@ -913,7 +916,7 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
font-size: 12px;
line-height: 1.4;
}
-.txt-info { margin-left: auto; font-size: 11px; color: var(--muted, rgba(255,255,255,0.62)); }
+.txt-info { margin-left: auto; font-size: 11px; color: var(--muted); }
/* Çalan parça durum satırı. Renk üç durumu ayırt ediyor: okunuyor ve bir şey
çalıyor (yeşil), okuma çalışıyor ama sessiz (sönük), okunamıyor (kırmızı).
Sebep görünmezse kullanıcı ayarlarda boşuna dolaşır. */
@@ -922,16 +925,16 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
.source-note { margin-left: auto; font-size: 11px; opacity: 0.6; white-space: nowrap; }
.np-status { font-variant-numeric: tabular-nums; }
-.np-status.ok { color: var(--ok, #5fd38d); }
-.np-status.bad { color: var(--bad, #ff7a7a); }
+.np-status.ok { color: var(--ok); }
+.np-status.bad { color: var(--danger-text); }
/* ===== Katman bayrakları, gruplar, A/B ===== */
.layer-flags { display: flex; align-items: center; gap: 3px; margin-left: auto; }
.btn.tiny.flagbtn { min-width: 24px; padding: 2px 5px; font-size: 10px; font-weight: 700; }
-.btn.tiny.flagbtn.on.solo { background: #f5a524; color: #1a1a1a; border-color: transparent; }
-.btn.tiny.flagbtn.on.mute { background: #e5484d; color: #fff; border-color: transparent; }
-.btn.tiny.flagbtn.on.lock { background: #6e56cf; color: #fff; border-color: transparent; }
+.btn.tiny.flagbtn.on.solo { background: var(--solo); color: var(--on-solo); border-color: transparent; }
+.btn.tiny.flagbtn.on.mute { background: var(--danger); color: var(--on-solid); border-color: transparent; }
+.btn.tiny.flagbtn.on.lock { background: var(--lock); color: var(--on-solid); border-color: transparent; }
.stack-item.locked { opacity: 0.6; }
/* ===== Katman satırı (#622) =====
@@ -944,12 +947,12 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
.stack-item.layer { padding: 0; overflow: hidden; transition: border-color .15s; }
.stack-item.layer.open { border-color: var(--line2); }
.layer-head { display: flex; align-items: center; gap: 8px; padding: 7px 10px; min-height: 46px; }
-.layer-head.open { background: rgba(255, 255, 255, .025); }
+.layer-head.open { background: rgb(var(--fg-rgb) / .025); }
.layer-ord { display: flex; flex-direction: column; gap: 2px; flex: none; }
.layer-ord .btn.tiny { padding: 0 5px; font-size: 8px; line-height: 13px; height: 15px; }
.layer-ico {
width: 30px; height: 30px; flex: none; display: grid; place-items: center;
- border-radius: 8px; background: rgba(255, 255, 255, .05); border: 1px solid var(--line); font-size: 14px;
+ border-radius: 8px; background: rgb(var(--fg-rgb) / .05); border: 1px solid var(--line); font-size: 14px;
}
.layer-name {
flex: 1; min-width: 0; text-align: left; background: none; border: 0; padding: 2px 0;
@@ -958,11 +961,11 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
.layer-name b, .layer-name small { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.layer-name b { font-size: 12.5px; font-weight: 650; }
.layer-name small { font-size: 10.5px; color: var(--muted2); margin-top: 2px; }
-.layer-name:hover b { color: #fff; }
+.layer-name:hover b { color: var(--text-strong); }
.layer-head .layer-flags { margin-left: 0; gap: 4px; }
.btn.tiny.layer-caret { width: 26px; padding: 3px 0; }
.btn.tiny.layer-del { color: var(--muted2); border-color: transparent; background: transparent; width: 26px; padding: 3px 0; }
-.btn.tiny.layer-del:hover { color: #ff8a8a; background: rgba(255, 60, 60, .12); border-color: rgba(255, 80, 80, .35); filter: none; }
+.btn.tiny.layer-del:hover { color: var(--danger-text); background: rgb(var(--danger-rgb) / .12); border-color: rgb(var(--danger-rgb) / .35); filter: none; }
.layer-body {
border-top: 1px solid var(--line); padding: 12px 14px 12px;
display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
@@ -984,7 +987,7 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
background: var(--card2); border: 1px solid var(--line); color: var(--muted); transition: .14s;
}
.layer-tab:hover { color: var(--text); border-color: var(--line2); }
-.layer-tab.active { background: var(--accent-soft); border-color: rgba(225, 29, 42, .45); color: #fff; }
+.layer-tab.active { background: var(--accent-soft); border-color: rgb(var(--accent-rgb) / .45); color: var(--text-strong); }
.layer-pane {
display: none; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 0 24px;
align-items: start; margin-top: 10px; padding: 12px; border-radius: 10px;
@@ -1012,16 +1015,17 @@ input.p-in[type='color'] { padding: 2px; height: 28px; width: 46px; flex: none;
.grp-panel { display: flex; flex-direction: column; gap: 8px; }
.grp-item {
- background: var(--card2, rgba(255, 255, 255, 0.03));
- border: 1px solid var(--line, rgba(255, 255, 255, 0.08));
+ background: var(--card2);
+ border: 1px solid var(--line);
border-radius: 9px;
padding: 7px 9px;
}
-.grp-count { margin-left: auto; font-size: 11px; color: var(--muted2, rgba(255, 255, 255, 0.45)); }
-.btn.tiny.on.mute { background: #e5484d; color: #fff; border-color: transparent; }
+.grp-count { margin-left: auto; font-size: 11px; color: var(--muted2); }
+.btn.tiny.on.mute { background: var(--danger); color: var(--on-solid); border-color: transparent; }
-.xf-box { background: var(--card2, rgba(255,255,255,0.03)); border: 1px solid var(--line, rgba(255,255,255,0.08)); border-radius: 9px; padding: 8px 9px; }
+.xf-box { background: var(--card2); border: 1px solid var(--line); border-radius: 9px; padding: 8px 9px; }
.xf-tag { font-weight: 700; font-size: 13px; }
-.xf-tag.dim { opacity: 0.35; }
+/* Boş taraf: saydamlık 2.9:1'e indiriyordu; ikincil yazı rengi okunur kalır (#770) */
+.xf-tag.dim { color: var(--muted2); font-weight: 400; }
.xf-val { margin: 0 auto; font-variant-numeric: tabular-nums; font-size: 12px; }
.xf-slider { width: 100%; margin: 6px 0; }
diff --git a/src/admin/texture-panel.js b/src/admin/texture-panel.js
index c396c007..5e285194 100644
--- a/src/admin/texture-panel.js
+++ b/src/admin/texture-panel.js
@@ -70,13 +70,13 @@
if (state.running && !state.error) {
st.appendChild(el('span', { icon: 'check' }));
st.appendChild(el('span', { text: 'Yayında' }));
- st.appendChild(el('span', { class: 'st-sep', text: ' · ' }));
+ st.appendChild(el('span', { class: 'st-sep', 'aria-hidden': 'true', text: ' · ' }));
st.appendChild(el('span', { class: 'st-num', text: state.width + '×' + state.height }));
- st.appendChild(el('span', { class: 'st-sep', text: ' · ' }));
+ st.appendChild(el('span', { class: 'st-sep', 'aria-hidden': 'true', text: ' · ' }));
st.appendChild(el('span', { class: 'st-num', text: String(state.frames || 0) + ' ' }));
st.appendChild(el('span', { text: 'kare' }));
if (state.dropped) {
- st.appendChild(el('span', { class: 'st-sep', text: ' · ' }));
+ st.appendChild(el('span', { class: 'st-sep', 'aria-hidden': 'true', text: ' · ' }));
st.appendChild(el('span', { class: 'st-num', text: String(state.dropped) + ' ' }));
st.appendChild(el('span', { text: 'düşen' }));
}
diff --git a/src/admin/theme-boot.js b/src/admin/theme-boot.js
new file mode 100644
index 00000000..21bd4987
--- /dev/null
+++ b/src/admin/theme-boot.js
@@ -0,0 +1,86 @@
+/*
+ * SoundVisualizer — CaYaDev Ses Görselleştirici
+ * Copyright (c) 2026 Çağan Turgut (CaYatur) — CaYaDev, https://cayadev.com
+ * https://github.com/CaYatur/SoundVisualizer
+ * SPDX-License-Identifier: MIT
+ */
+'use strict';
+/* ARAYÜZ TEMASINI UYGULA (#770).
+
+ içinde, gövde çizilmeden önce çalışır: ana süreç seçili temayı
+ adresin `uiTheme` sorgusuyla verir, böylece aydınlık bir tema koyu bir
+ karenin ardından gelmez. Ayarlar yüklenince admin.js `SVUiTheme.set`
+ ile ayardaki değeri uygular; adres de güncellenir ki dil değişikliğinin
+ yeniden yüklemesi eski temayla açılmasın.
+
+ "Cihaz varsayılanı" işletim sisteminin tercihini `prefers-color-scheme`
+ ile okur ve değişince canlı izler. `nativeTheme.themeSource`
+ değiştirilmez: o, görselleştirici dahil her pencerenin tercihini
+ değiştirirdi.
+
+ Tuvale çizen paneller (zaman çizelgesi, eşleme) rengi buradan alır ve
+ `onChange` ile yeniden çizer. */
+(() => {
+ const T = window.SVUiThemes;
+ if (!T) return;
+ const mq = typeof window.matchMedia === 'function' ? window.matchMedia('(prefers-color-scheme: dark)') : null;
+ const listeners = new Set();
+ let choice = T.DEFAULT_ID;
+ let theme = null;
+
+ function prefersDark() {
+ return mq ? mq.matches : true;
+ }
+
+ function rememberInUrl() {
+ try {
+ const u = new URL(window.location.href);
+ if (u.searchParams.get('uiTheme') === choice) return;
+ u.searchParams.set('uiTheme', choice);
+ window.history.replaceState(window.history.state, '', u.toString());
+ } catch { /* file: adresinde geçmiş yazılamıyorsa ilk çizim yine ayardan */ }
+ }
+
+ function set(id) {
+ choice = T.normalizeId(id);
+ const prev = theme ? theme.id : null;
+ theme = T.apply(document, choice, prefersDark());
+ rememberInUrl();
+ if (prev !== null && prev !== theme.id) {
+ listeners.forEach((fn) => { try { fn(theme); } catch (e) { console.error('[theme]', e); } });
+ }
+ return theme;
+ }
+
+ // Değişkenin değeri: '#rrggbb' ya da 'rgba(...)'
+ function color(name) {
+ const t = theme || T.resolve(choice, prefersDark());
+ return t.tokens[name];
+ }
+
+ // Değişkeni saydamlıkla: kanal ('255 255 255') ya da renk değişkeni
+ function alpha(name, a) {
+ const c = T.parseColor(color(name));
+ if (!c) return 'transparent';
+ return 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + a + ')';
+ }
+
+ if (mq) {
+ const onScheme = () => { if (choice === T.SYSTEM_ID) set(choice); };
+ if (typeof mq.addEventListener === 'function') mq.addEventListener('change', onScheme);
+ else if (typeof mq.addListener === 'function') mq.addListener(onScheme);
+ }
+
+ let initial = null;
+ try { initial = new URLSearchParams(window.location.search).get('uiTheme'); } catch { initial = null; }
+ set(initial || T.DEFAULT_ID);
+
+ window.SVUiTheme = {
+ set,
+ choice: () => choice,
+ current: () => theme,
+ color,
+ alpha,
+ onChange: (fn) => { listeners.add(fn); return () => listeners.delete(fn); },
+ };
+})();
diff --git a/src/admin/timeline-panel.js b/src/admin/timeline-panel.js
index 54a5bb19..50ddf421 100644
--- a/src/admin/timeline-panel.js
+++ b/src/admin/timeline-panel.js
@@ -332,13 +332,13 @@
return v.scroll + (x - HEAD_W) / v.zoom;
}
- function css(name, fallback) {
- try {
- const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
- return v || fallback;
- } catch (e) {
- return fallback;
- }
+ /* Renkler arayüz temasından (#770). Tuval CSS'i görmez; tema değişince
+ yeniden çizilir (aşağıda onChange). */
+ function css(name) {
+ return window.SVUiTheme ? window.SVUiTheme.color(name) : '';
+ }
+ function tint(name, a) {
+ return window.SVUiTheme ? window.SVUiTheme.alpha(name, a) : 'transparent';
}
function rgba(hex, a) {
@@ -377,10 +377,11 @@
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
- const fg = css('--text', '#e8e8ec');
- const dim = css('--muted', '#8b8b96');
- const line = css('--line', '#2a2a33');
- const accent = css('--accent', '#e11d2a');
+ const fg = css('--text');
+ const dim = css('--muted');
+ const line = css('--line');
+ const accent = css('--accent');
+ const strong = css('--text-strong');
const solo = TL().anySolo ? TL().anySolo(tl) : false;
// --- Şerit zeminleri: tek ve çift şerit ayrı tonda, seçili parça belirgin ---
@@ -388,7 +389,7 @@
const y = RULER_H + i * TRACK_H;
const trk = tl.tracks[i];
const picked = selection && selection.trackId === trk.id;
- ctx.fillStyle = picked ? 'rgba(255,255,255,.05)' : i % 2 ? 'rgba(255,255,255,.018)' : 'rgba(0,0,0,0)';
+ ctx.fillStyle = picked ? tint('--fg-rgb', 0.05) : i % 2 ? tint('--fg-rgb', 0.018) : 'transparent';
ctx.fillRect(0, y, w, TRACK_H);
}
@@ -404,12 +405,12 @@
const lw = (tl.loop.end - tl.loop.start) * view().zoom;
const on = tl.loop.enabled;
if (on) {
- ctx.fillStyle = 'rgba(120,180,255,.08)';
+ ctx.fillStyle = tint('--info-rgb', 0.08);
ctx.fillRect(lx, RULER_H, lw, h - RULER_H);
}
- ctx.fillStyle = on ? 'rgba(120,180,255,.55)' : 'rgba(120,180,255,.2)';
+ ctx.fillStyle = tint('--info-rgb', on ? 0.55 : 0.2);
ctx.fillRect(lx, 0, lw, LOOP_BAND);
- ctx.fillStyle = on ? 'rgba(160,205,255,.95)' : 'rgba(160,205,255,.4)';
+ ctx.fillStyle = tint('--info-rgb', on ? 0.95 : 0.4);
ctx.fillRect(lx - 1, 0, 3, LOOP_BAND + 3);
ctx.fillRect(lx + lw - 2, 0, 3, LOOP_BAND + 3);
}
@@ -434,14 +435,14 @@
for (const m of tl.markers) {
const x = xOf(m.t);
if (x < -2 || x > w + 2) continue;
- ctx.strokeStyle = 'rgba(240,180,41,.7)';
+ ctx.strokeStyle = tint('--cue-armed', 0.7);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(Math.round(x) + 0.5, RULER_H);
ctx.lineTo(Math.round(x) + 0.5, h);
ctx.stroke();
// Bayrak cetvelin alt bandında: ölçü ve süre yazılarının altında
- ctx.fillStyle = '#f0b429';
+ ctx.fillStyle = css('--cue-armed');
ctx.beginPath();
ctx.moveTo(x, RULER_H - 11);
ctx.lineTo(x + 7, RULER_H - 7);
@@ -460,13 +461,13 @@
const x = xOf(e.t);
if (x < -TEMPO_TAG_W || x > w + 2) continue;
const on = i === tsel;
- ctx.strokeStyle = on ? '#b8f6ff' : 'rgba(80,210,230,.85)';
+ ctx.strokeStyle = on ? strong : tint('--tempo', 0.85);
ctx.lineWidth = on ? 2 : 1;
ctx.beginPath();
ctx.moveTo(Math.round(x) + 0.5, RULER_H - 14);
ctx.lineTo(Math.round(x) + 0.5, h);
ctx.stroke();
- ctx.fillStyle = on ? '#b8f6ff' : '#50d2e6';
+ ctx.fillStyle = on ? strong : css('--tempo');
const sig = e.beatsPerBar !== tl.tempo[i - 1].beatsPerBar ? ' ' + e.beatsPerBar + '/4' : '';
ctx.fillText('♩' + (Math.round(e.bpm * 10) / 10) + sig, x + 3, RULER_H - 4);
}
@@ -492,9 +493,9 @@
if (drag && drag.kind === 'marquee' && drag.moved) {
const x0 = Math.min(drag.x0, drag.x1);
const y0 = Math.min(drag.y0, drag.y1);
- ctx.fillStyle = 'rgba(255,255,255,.06)';
+ ctx.fillStyle = tint('--fg-rgb', 0.06);
ctx.fillRect(x0, y0, Math.abs(drag.x1 - drag.x0), Math.abs(drag.y1 - drag.y0));
- ctx.strokeStyle = 'rgba(255,255,255,.55)';
+ ctx.strokeStyle = tint('--fg-rgb', 0.55);
ctx.lineWidth = 1;
ctx.setLineDash([4, 3]);
ctx.strokeRect(x0 + 0.5, y0 + 0.5, Math.abs(drag.x1 - drag.x0), Math.abs(drag.y1 - drag.y0));
@@ -509,7 +510,7 @@
function drawRuler(w, h, tl, fg, dim, line) {
const cfg = tlCfg();
const v = view();
- ctx.fillStyle = 'rgba(255,255,255,.04)';
+ ctx.fillStyle = tint('--fg-rgb', 0.04);
ctx.fillRect(0, 0, w, RULER_H);
const showBars = cfg.ruler !== 'time';
@@ -519,7 +520,7 @@
const b0 = TL().secondsToBars(tl.tempo, v.scroll);
const barSec = TL().barsToSeconds(tl.tempo, b0.bar + 1, 1) - TL().barsToSeconds(tl.tempo, b0.bar, 1);
if (barSec * v.zoom >= 14) {
- ctx.strokeStyle = 'rgba(255,255,255,.05)';
+ ctx.strokeStyle = tint('--fg-rgb', 0.05);
ctx.lineWidth = 1;
for (let bar = b0.bar; ; bar++) {
const x = Math.round(xOf(TL().barsToSeconds(tl.tempo, bar, 1))) + 0.5;
@@ -631,14 +632,14 @@
tamamlanıyor. */
const fw = Math.min(cw, (c.fade || 0) * view().zoom);
if (fw > 1) {
- ctx.fillStyle = 'rgba(0,0,0,.28)';
+ ctx.fillStyle = tint('--shadow-rgb', 0.28);
ctx.beginPath();
ctx.moveTo(x, top);
ctx.lineTo(x + fw, top);
ctx.lineTo(x, top + hh);
ctx.closePath();
ctx.fill();
- ctx.strokeStyle = 'rgba(255,255,255,.55)';
+ ctx.strokeStyle = tint('--fg-rgb', 0.55);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x, top + hh);
@@ -646,12 +647,12 @@
ctx.stroke();
}
if ((sel || fw > 1) && cw > 14) {
- ctx.fillStyle = sel ? '#ffffff' : 'rgba(255,255,255,.7)';
+ ctx.fillStyle = sel ? css('--text-strong') : tint('--fg-rgb', 0.7);
ctx.fillRect(x + fw - 3, top - 1, 6, 6);
}
// Kaynağı seçilmemiş klip çizgili: ateşlendiğinde hiçbir şey olmayacak
if (!c.ref) {
- ctx.strokeStyle = 'rgba(255,255,255,.18)';
+ ctx.strokeStyle = tint('--fg-rgb', 0.18);
ctx.lineWidth = 1;
ctx.save();
ctx.beginPath();
@@ -665,7 +666,7 @@
}
ctx.restore();
}
- ctx.strokeStyle = sel ? '#ffffff' : rgba(col, 0.9);
+ ctx.strokeStyle = sel ? css('--text-strong') : rgba(col, 0.9);
ctx.lineWidth = sel ? 2 : 1;
ctx.strokeRect(x + 0.5, top + 0.5, cw - 1, hh - 1);
if (cw > 26) {
@@ -680,7 +681,7 @@
ctx.fillText(clipLabel(c), x + (ico && window.SVIcons ? 22 : 6), top + 16);
if (cw > 80) {
ctx.font = '10px system-ui, sans-serif';
- ctx.fillStyle = 'rgba(255,255,255,.6)';
+ ctx.fillStyle = tint('--fg-rgb', 0.6);
ctx.fillText(fmtTime(c.dur) + (c.fade > 0 ? ' · ' + tt('geçiş') + ' ' + c.fade.toFixed(2) : ''), x + 6, top + hh - 5);
}
ctx.restore();
@@ -718,7 +719,7 @@
if (x < -6 || x > w + 6) continue;
const yy = bot - (bot - top) * Math.max(0, Math.min(1, k.v));
const sel = selection && selection.kind === 'key' && selection.trackId === trk.id && selection.keyIndex === ki;
- ctx.fillStyle = sel ? '#fff' : col;
+ ctx.fillStyle = sel ? css('--text-strong') : col;
ctx.beginPath();
ctx.arc(x, yy, sel ? 4.5 : 3.5, 0, Math.PI * 2);
ctx.fill();
@@ -2365,4 +2366,7 @@
box.appendChild(marks);
return box;
}
+
+ // Tema değişince tuval yeni renklerle (#770)
+ if (window.SVUiTheme) window.SVUiTheme.onChange(() => draw());
})();
diff --git a/src/main/main.js b/src/main/main.js
index 76e00e02..0f8b6d3d 100644
--- a/src/main/main.js
+++ b/src/main/main.js
@@ -6,7 +6,7 @@
*/
'use strict';
-const { app, BrowserWindow, ipcMain, screen, dialog, protocol, net, shell, powerSaveBlocker } = require('electron');
+const { app, BrowserWindow, ipcMain, screen, dialog, protocol, net, shell, powerSaveBlocker, nativeTheme } = require('electron');
const path = require('path');
const fs = require('fs');
const url = require('url');
@@ -34,6 +34,7 @@ const logoLibrary = require('./logo-library');
const mediaLibrary = require('./media-library');
const lyricsLibrary = require('./lyrics-library');
const lyricsSync = require('../shared/lyrics-sync');
+const uiThemes = require('../shared/ui-themes');
const mcpServer = require('./mcp-server');
const faultGuardLib = require('./fault-guard');
@@ -572,6 +573,8 @@ function installSignalHandlers() {
}
installSignalHandlers();
app.whenReady().then(() => { installSignalHandlers(); setTimeout(installSignalHandlers, 1000); });
+// "Cihaz varsayılanı" teması: sistem açık/koyu değişince pencere zemini de (#770)
+app.whenReady().then(() => { nativeTheme.on('updated', syncAdminBackground); });
ipcMain.on('confirm-close-approved', () => {
forceQuitApp = true;
@@ -619,8 +622,31 @@ async function confirmCloseIfNeeded(parentWin) {
}
}
+/* ARAYÜZ TEMASI (#770). Panel penceresinin zemini temanın zemini: pencere
+ ilk karesini çizmeden önce bu renkle görünür, aydınlık temada koyu bir
+ kare parlamasın. README görüntüleri her zaman CaYaDev. "Cihaz varsayılanı"
+ işletim sisteminin tercihini okur; nativeTheme.themeSource değiştirilmez
+ (görselleştiricinin tercihini de değiştirirdi). */
+function adminUiTheme(cfg) {
+ if (SHOTS) return uiThemes.DEFAULT_ID;
+ return uiThemes.normalizeId(cfg && cfg.ui && cfg.ui.theme);
+}
+function adminBackground(cfg) {
+ return uiThemes.resolve(adminUiTheme(cfg), nativeTheme.shouldUseDarkColors).tokens['--bg'];
+}
+let adminBgShown = null;
+function syncAdminBackground() {
+ if (!adminWin || adminWin.isDestroyed()) return;
+ const bg = adminBackground(currentConfig);
+ if (bg === adminBgShown) return;
+ adminBgShown = bg;
+ try { adminWin.setBackgroundColor(bg); } catch { /* pencere kapanıyor */ }
+}
+
function createAdminWindow() {
const iconPath = path.join(__dirname, '..', '..', 'build', 'icon.ico');
+ const startCfg = currentConfig || loadSettings();
+ adminBgShown = adminBackground(startCfg);
adminWin = new BrowserWindow({
// Üç sütunlu düzen (kategori rayı + çalışma alanı + önizleme dock'u) için
// daha geniş varsayılan pencere
@@ -633,7 +659,7 @@ function createAdminWindow() {
minHeight: 560,
icon: fs.existsSync(iconPath) ? iconPath : undefined,
title: trUi('Ses Görselleştirici — Yönetici Paneli', 'Sound Visualizer — Admin Panel'),
- backgroundColor: '#0b0910',
+ backgroundColor: adminBgShown,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, 'preload-admin.js'),
@@ -647,7 +673,7 @@ function createAdminWindow() {
},
});
- adminWin.loadFile(path.join(__dirname, '..', 'admin', 'index.html'));
+ adminWin.loadFile(path.join(__dirname, '..', 'admin', 'index.html'), { query: { uiTheme: adminUiTheme(startCfg) } });
attachSmoke(adminWin, 'ADMIN');
if (process.argv.includes('--dev')) {
@@ -1830,7 +1856,12 @@ ipcMain.handle('repair-audio', async () => {
return { ok: after.ok, repaired: after.ok, diagnostic: after, install };
});
-ipcMain.handle('get-settings', () => loadSettings());
+ipcMain.handle('get-settings', () => {
+ const s = loadSettings();
+ // README görüntüleri kullanıcının temasıyla değil, CaYaDev ile (#770)
+ if (SHOTS && s && typeof s === 'object') s.ui = Object.assign({}, s.ui, { theme: uiThemes.DEFAULT_ID });
+ return s;
+});
// ----------------------------------------------------------------------------
// Yüzen / resim-içinde-resim penceresi
@@ -2491,6 +2522,7 @@ function applyIncomingConfig(config, opts) {
const prevLightingJson = JSON.stringify((currentConfig && currentConfig.lighting) || null);
currentConfig = config;
if (save) saveSettings(config);
+ syncAdminBackground();
/* Every config push used to call lightingSet; in dynamic modes that hits
addon.setAll(baseLevel) and flickers the lights. Re-apply only when the
lighting tree actually changed. */
@@ -7114,6 +7146,68 @@ async function runSmoke() {
await awc3.executeJavaScript("localStorage.removeItem('sv-language')");
}
+ /* --- Arayüz temaları: ekrandaki yazıların karşıtlığı (#770) ---
+ Her tema panelin kendi seçicisinden seçilir, bütün kategoriler
+ kaydırılarak taranır ve doğrudan yazı taşıyan her görünür öğenin
+ tarayıcının çizdiği rengi zeminiyle ölçülür (src/shared/ui-contrast-audit.js).
+ Statik testler tablodaki ikilileri ölçüyor; bu, satır içi bir renk ya da
+ yanlış zemine konmuş bir yazı gibi yalnız çizimde görünen hatayı yakalar.
+ Sonunda kullanıcının teması geri seçilir. */
+ if (adminWin && !adminWin.isDestroyed()) {
+ const awc4 = adminWin.webContents;
+ try {
+ await awc4.executeJavaScript(fs.readFileSync(path.join(__dirname, '..', 'shared', 'ui-contrast-audit.js'), 'utf8') + '; 1');
+ const report = await awc4.executeJavaScript(`(async () => {
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+ const A = window.SVUiContrastAudit, U = window.SVUiTheme, UT = window.SVUiThemes;
+ if (!A || !U || !UT) return { error: 'theme scripts missing' };
+ const click = (sel) => { const e = document.querySelector(sel); if (e) e.click(); return !!e; };
+ const original = U.choice();
+ const cats = Array.from(document.querySelectorAll('[data-cat]')).map((b) => b.dataset.cat);
+ const out = [];
+ for (const t of UT.THEMES) {
+ click('[data-cat="settings"]'); await sleep(450);
+ if (!click('.ui-theme-opt[data-theme="' + t.id + '"]')) { out.push({ theme: t.id, error: 'not in the picker' }); continue; }
+ await sleep(250);
+ let checked = 0, min = Infinity;
+ const fails = [];
+ for (const c of cats) {
+ click('[data-cat="' + c + '"]'); await sleep(400);
+ const s = document.getElementById('sections');
+ const steps = Math.min(8, Math.ceil(s.scrollHeight / Math.max(1, s.clientHeight)));
+ for (let k = 0; k < steps; k++) {
+ s.scrollTop = k * s.clientHeight; await sleep(100);
+ const r = A.audit(document, window, { pageBg: getComputedStyle(document.body).backgroundColor, aaa: !!U.current().aaa });
+ checked += r.checked;
+ if (r.min < min) min = r.min;
+ r.fails.forEach((f) => { if (!fails.some((x) => x.path === f.path && x.text === f.text)) fails.push(Object.assign({ cat: c }, f)); });
+ }
+ }
+ out.push({ theme: t.id, applied: document.documentElement.getAttribute('data-ui-theme'), checked, min, failCount: fails.length, fails: fails.slice(0, 8) });
+ }
+ click('[data-cat="settings"]'); await sleep(350);
+ click('.ui-theme-opt[data-theme="' + original + '"]');
+ await sleep(200);
+ return { themes: out, restored: U.choice() === original };
+ })()`);
+ if (!report || report.error) {
+ errors.push('ui themes: ' + ((report && report.error) || 'no report'));
+ } else {
+ report.themes.forEach((r) => {
+ if (r.error) { errors.push('ui theme ' + r.theme + ': ' + r.error); return; }
+ console.log('[SMOKE] tema ' + r.theme + ': ' + r.checked + ' yazı, en düşük karşıtlık ' + r.min + ', eşik altı ' + r.failCount);
+ r.fails.forEach((f) => console.log('[SMOKE] ~ ' + f.cat + ' ' + f.ratio + ' < ' + f.need + ' ' + f.path + ' «' + f.text + '»'));
+ if (r.applied !== r.theme) errors.push('ui theme ' + r.theme + ' was not applied (' + r.applied + ')');
+ if (r.checked < 500) errors.push('ui theme ' + r.theme + ': only ' + r.checked + ' text elements measured');
+ if (r.failCount) errors.push('ui theme ' + r.theme + ': ' + r.failCount + ' text element(s) below the contrast threshold');
+ });
+ if (!report.restored) errors.push('ui themes: the user\'s theme was not restored');
+ }
+ } catch (e) {
+ errors.push('ui themes: ' + (e && e.message ? e.message : e));
+ }
+ }
+
/* --- Panel ekran görüntüleri (--smoke-shots) ---
Görsel bir gözle bakılmadan "çizildi" demek yetmiyor: kartların taşması,
boş kalan bir sütun ya da okunmaz bir kod editörü ancak resimde görünür. */
diff --git a/src/shared/defaults.js b/src/shared/defaults.js
index a649957c..708f0e80 100644
--- a/src/shared/defaults.js
+++ b/src/shared/defaults.js
@@ -326,6 +326,14 @@
mode: 'notify',
},
+ /* Panelin arayüz teması (#770): src/shared/ui-themes.js'teki bir kimlik
+ ya da 'system' (cihazın açık/koyu tercihi). Görselleştirici çıktısını
+ etkilemez. "Varsayılanlara Sıfırla" bunu korur: okunabilirlik için
+ seçilmiş bir tema sıfırlamayla kaybolmamalı. */
+ ui: {
+ theme: 'cayadev',
+ },
+
power: {
// 0 = ekranla eşitle (her yenilemede bir kare). En akıcı sonuç budur:
// ekranın yenileme hızının tam böleni olmayan bir sınır (75 Hz'de 60 gibi)
diff --git a/src/shared/i18n.js b/src/shared/i18n.js
index acc5f9e1..e929b565 100644
--- a/src/shared/i18n.js
+++ b/src/shared/i18n.js
@@ -2976,6 +2976,15 @@
/* Dışa aktarma profilleri (#522); export-formats.js etiketleriyle aynı */
'İşaretçilerden bölümlere ayır': 'Split into chapters at the markers',
'Hazır Profil': 'Preset Profile',
+ /* Arayüz teması (#770). Karanlık ve Aydınlık palet gruplarından. */
+ 'Dil, arayüz teması, pencere koruması, genişletilmiş aralıklar ve güncellemeler.': 'Language, interface theme, window protection, extended ranges and updates.',
+ 'Arayüz Teması': 'Interface Theme',
+ 'Cihaz varsayılanı': 'Device default',
+ 'Yüksek Karşıtlık': 'High Contrast',
+ 'Gece Mavisi': 'Midnight',
+ 'Sepya': 'Sepia',
+ 'Cihazınız şu an {0} kullanıyor; değişince panel de değişir.': 'Your device is using {0} right now; the panel follows it when it changes.',
+ 'Yalnız paneli değiştirir; görselleştirici çıktısı, yayın katmanı ve kendi renkleriniz etkilenmez.': 'Changes the panel only; the visualizer output, the stream overlay and your own colours are not affected.',
/* İki render kipi (#530) */
'Zaman çizelgesini izle (çalınca başla, durunca bitir)': 'Follow the timeline (start on play, stop on stop)',
'Gerçek zamanlı kip: ekranda göründüğü gibi, canlı sesle kaydedin; modülasyon, geçiş ve efektler dahil. Zaman çizelgesini izleyebilir. MP4, WebM, GIF ve PNG.': 'Realtime mode: record what is on screen with the live audio, including modulation, transitions and effects. It can follow the timeline. MP4, WebM, GIF and PNG.',
diff --git a/src/shared/mcp.js b/src/shared/mcp.js
index 191b9a8f..16d5c5a6 100644
--- a/src/shared/mcp.js
+++ b/src/shared/mcp.js
@@ -183,6 +183,8 @@
export: 'export', recording: 'export',
display: 'output', mapping: 'output', textureShare: 'output', aspect: 'output', floating: 'output',
power: 'output', lighting: 'output', openrgb: 'output', artnet: 'output', oscOut: 'output',
+ // Panelin arayüz teması (#770): uygulama ayarı, power ile aynı izin
+ ui: 'output',
'audio.sources': 'output',
'background.transparent': 'output', 'background.coverTaskbar': 'output', 'background.transparentKey': 'output',
postfx: 'effectEdit', modulation: 'effectEdit',
diff --git a/src/shared/ui-contrast-audit.js b/src/shared/ui-contrast-audit.js
new file mode 100644
index 00000000..5937a4f3
--- /dev/null
+++ b/src/shared/ui-contrast-audit.js
@@ -0,0 +1,175 @@
+/*
+ * SoundVisualizer — CaYaDev Ses Görselleştirici
+ * Copyright (c) 2026 Çağan Turgut (CaYatur) — CaYaDev, https://cayadev.com
+ * https://github.com/CaYatur/SoundVisualizer
+ * SPDX-License-Identifier: MIT
+ */
+'use strict';
+/* EKRANDAKİ YAZILARIN KARŞITLIĞI (#770).
+
+ Öz test her temada panelin bütün kategorilerini açar ve bu tarayıcıyı
+ çalıştırır. Statik testler tablodaki değişken ikililerini ölçer; bu ise
+ tarayıcının GERÇEKTEN çizdiği rengi ölçer: bir panel satır içi bir renk
+ yazarsa ya da bir yazıyı yanlış zemine koyarsa burada görünür.
+
+ Yöntem: doğrudan yazı taşıyan her görünür öğe için hesaplanan `color` ve
+ atalarından toplanan zemin katmanları (background-color ve geçişlerin
+ renk durakları) temanın --bg rengi üstüne bindirilir. Geçişte durakların
+ ortalaması alınır (yazı genelde ortada durur). Medya önizlemesi gibi
+ görüntü üstündeki alanlar ve devre dışı denetimler sayılmaz (WCAG 1.4.3
+ devre dışı denetimleri muaf tutar).
+
+ Eşik: 4.5:1; büyük yazı (24 px ya da 18.66 px kalın) 3:1. AAA temada 7 ve
+ 4.5. */
+(function () {
+ const parse = (s) => {
+ const m = /rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)/.exec(String(s || ''));
+ if (!m) return null;
+ let a = m[4] === undefined ? 1 : m[4].endsWith('%') ? parseFloat(m[4]) / 100 : parseFloat(m[4]);
+ if (!Number.isFinite(a)) a = 1;
+ return [+m[1], +m[2], +m[3], a];
+ };
+
+ // Geçişin renk durakları (hesaplanan değer rgb()/rgba() biçiminde gelir)
+ function gradientStops(img) {
+ if (!img || img === 'none' || img.indexOf('gradient') < 0 || img.indexOf('url(') >= 0) return null;
+ const re = /rgba?\([^)]*\)/g;
+ const stops = [];
+ let m;
+ while ((m = re.exec(img))) {
+ const c = parse(m[0]);
+ if (c) stops.push(c);
+ }
+ return stops.length ? stops : null;
+ }
+
+ function over(top, base) {
+ const a = top[3];
+ return [0, 1, 2].map((i) => top[i] * a + base[i] * (1 - a)).concat(1);
+ }
+
+ /* Durakların ortalaması saydamlıkla ağırlıklı: `transparent` durağı
+ (rgba(0,0,0,0)) rengi karartmamalı, yalnız örtüyü inceltmeli. */
+ function mean(list) {
+ const n = list.length;
+ const a = list.reduce((s, c) => s + c[3], 0);
+ if (a <= 0) return [0, 0, 0, 0];
+ return [0, 1, 2].map((i) => list.reduce((s, c) => s + c[i] * c[3], 0) / a).concat(a / n);
+ }
+
+ function luminance(c) {
+ const ch = c.slice(0, 3).map((v) => {
+ const x = v / 255;
+ return x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
+ });
+ return 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2];
+ }
+
+ function contrast(a, b) {
+ const la = luminance(a);
+ const lb = luminance(b);
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
+ }
+
+ /* Katmanlar yukarıdan aşağıya: [0] yazının hemen arkası. İlk opak katman
+ taban olur, üstündekiler sırayla bindirilir; opak katman yoksa taban
+ sayfanın zemini. */
+ function backdrop(layers, pageBg) {
+ const i = layers.findIndex((l) => l[3] >= 0.999);
+ let base = i >= 0 ? layers[i] : pageBg;
+ for (let j = (i >= 0 ? i : layers.length) - 1; j >= 0; j--) base = over(layers[j], base);
+ return base;
+ }
+
+ function threshold(px, weight, aaa) {
+ const large = px >= 24 || (px >= 18.66 && weight >= 700);
+ if (aaa) return large ? 4.5 : 7;
+ return large ? 3 : 4.5;
+ }
+
+ // Görüntü/medya üstündeki alanlar (yazı orada görüntünün üstünde durur)
+ const SKIP = [
+ '.preview-stage', '.studio-preview', '.md-thumb-box', '.logo-lib-thumb', '.img-thumb',
+ '.scene-thumb', '.layer-preview', '.md-cell .md-fav', '.preset', '.up-swatch',
+ '.tpl-swatch', '.tl-swatch', '.cd-cell', '.perf-cell', '[aria-hidden="true"]',
+ '.ui-theme-preview',
+ ].join(',');
+
+ function audit(doc, win, opts) {
+ const o = opts || {};
+ const pageBg = parse(o.pageBg) || [0, 0, 0, 1];
+ /* Tema değişince renkler CSS geçişiyle (0.15 sn) değişiyor; arka plandaki
+ bir pencerede geçiş ilerlemiyor ve eski temanın rengi ölçülüyordu.
+ Ölçmeden önce bekleyen geçişler sonuna alınır. */
+ if (typeof doc.getAnimations === 'function') {
+ doc.getAnimations().forEach((an) => {
+ if (an.constructor && an.constructor.name === 'CSSTransition') { try { an.finish(); } catch { /* sonsuz değil */ } }
+ });
+ }
+ const fails = [];
+ let checked = 0;
+ let min = Infinity;
+ const all = doc.body.querySelectorAll('*');
+ for (let k = 0; k < all.length; k++) {
+ const el = all[k];
+ let hasText = false;
+ for (let n = el.firstChild; n; n = n.nextSibling) {
+ if (n.nodeType === 3 && n.nodeValue.trim()) { hasText = true; break; }
+ }
+ if (!hasText) continue;
+ if (/^(SCRIPT|STYLE|OPTION|TEXTAREA|CANVAS|svg)$/i.test(el.tagName)) continue;
+ if (el.closest(SKIP)) continue;
+ if (el.closest(':disabled, [aria-disabled="true"], .disabled')) continue;
+ const rect = el.getBoundingClientRect();
+ if (rect.width < 1 || rect.height < 1) continue;
+ if (rect.bottom < 0 || rect.top > win.innerHeight || rect.right < 0 || rect.left > win.innerWidth) continue;
+ const cs = win.getComputedStyle(el);
+ if (cs.visibility !== 'visible' || cs.display === 'none') continue;
+ let fg = parse(cs.color);
+ if (!fg) continue;
+ // Opaklık: öğe ve ataları (devre dışı görünüm, soluk yazılar)
+ let alpha = 1;
+ const layers = [];
+ let hidden = false;
+ for (let p = el; p && p.nodeType === 1; p = p.parentElement) {
+ const ps = p === el ? cs : win.getComputedStyle(p);
+ if (ps.display === 'none' || ps.visibility === 'hidden') { hidden = true; break; }
+ alpha *= parseFloat(ps.opacity);
+ // Aynı öğede geçiş, zemin renginin üstünde çizilir
+ const stops = gradientStops(ps.backgroundImage);
+ if (stops) layers.push(mean(stops));
+ const bc = parse(ps.backgroundColor);
+ if (bc && bc[3] > 0) layers.push(bc);
+ }
+ if (hidden || alpha < 0.05) continue;
+ // Kendi saydamlığıyla neredeyse görünmeyen süs yazıları (yer tutucu çizgiler)
+ if (fg[3] * alpha < 0.05) continue;
+ const bg = backdrop(layers, pageBg);
+ fg = over([fg[0], fg[1], fg[2], fg[3] * alpha], bg);
+ const ratio = contrast(fg, bg);
+ checked++;
+ const need = threshold(parseFloat(cs.fontSize), parseInt(cs.fontWeight, 10) || 400, !!o.aaa);
+ if (ratio < min) min = ratio;
+ if (ratio < need) {
+ const text = Array.from(el.childNodes).filter((n) => n.nodeType === 3).map((n) => n.nodeValue.trim()).join(' ').slice(0, 60);
+ fails.push({ path: describe(el), text, ratio: Math.round(ratio * 100) / 100, need });
+ }
+ }
+ return { checked, min: Math.round(min * 100) / 100, fails };
+ }
+
+ function describe(el) {
+ const parts = [];
+ for (let p = el; p && p.nodeType === 1 && parts.length < 4; p = p.parentElement) {
+ let s = p.tagName.toLowerCase();
+ if (p.id) s += '#' + p.id;
+ else if (p.classList && p.classList.length) s += '.' + Array.from(p.classList).slice(0, 2).join('.');
+ parts.unshift(s);
+ }
+ return parts.join(' > ');
+ }
+
+ const api = { audit, parse, gradientStops, mean, backdrop, over, contrast, threshold };
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
+ if (typeof window !== 'undefined') window.SVUiContrastAudit = api;
+})();
diff --git a/src/shared/ui-theme-contrast.js b/src/shared/ui-theme-contrast.js
new file mode 100644
index 00000000..26d06800
--- /dev/null
+++ b/src/shared/ui-theme-contrast.js
@@ -0,0 +1,122 @@
+/*
+ * SoundVisualizer — CaYaDev Ses Görselleştirici
+ * Copyright (c) 2026 Çağan Turgut (CaYatur) — CaYaDev, https://cayadev.com
+ * https://github.com/CaYatur/SoundVisualizer
+ * SPDX-License-Identifier: MIT
+ */
+'use strict';
+/* TEMA KARŞITLIK KURALLARI (#770).
+
+ Her tema için hangi yazı hangi zeminde okunmalı, eşik ne. Testler ve
+ öz test bu listeyi kullanır; yeni bir yazı rengi ya da yüzey eklenirse
+ buraya da girer.
+
+ Eşikler WCAG 2.2'den:
+ - yazı 4.5:1 (panelin yazılarının çoğu 10.5–13 px, yani "küçük yazı");
+ - anlam taşıyan çizgi, odak halkası, düğme topuzu 3:1 (1.4.11);
+ - Yüksek Karşıtlık temasında yazı 7:1 (AAA). */
+(function () {
+ const T = typeof module !== 'undefined' && module.exports ? require('./ui-themes') : window.SVUiThemes;
+ const SURFACES = ['--bg', '--bg2', '--surface', '--card', '--card2', '--card3'];
+ const TEXT = 4.5;
+ const AAA = 7;
+ const UI = 3;
+
+ /* Kurallar: { fg, on, min, why }. `on` bir değişken adı ya da
+ { tint: kanal, alpha, base } (yarı saydam örtülü zemin). `min` 'text'
+ ise tema AAA'ysa 7, değilse 4.5. */
+ function rules() {
+ const out = [];
+ const text = (fg, on, why) => out.push({ fg, on, min: 'text', why });
+ const ui = (fg, on, why) => out.push({ fg, on, min: UI, why });
+
+ // Gövde yazısı ve ikincil yazılar her yüzeyde
+ ['--text', '--muted', '--muted2', '--accent2', '--ok', '--warn', '--cue-armed',
+ '--danger-text', '--ok-text', '--warn-text', '--accent-text', '--accent-label'].forEach((fg) => {
+ SURFACES.forEach((s) => text(fg, s, 'yazı ' + fg + ' / ' + s));
+ });
+ text('--text', '--popover', 'arama sonuçları ve açılır menü');
+ text('--muted2', '--popover', 'arama sonucu yolu');
+ text('--text-strong', '--card3', 'seçili satır');
+ // Örtülü zeminlerdeki durum yazıları (rozet, bildirim, bant)
+ [['--accent-text', '--accent-rgb', 0.22], ['--accent-label', '--accent-rgb', 0.14],
+ ['--ok-text', '--ok-rgb', 0.14], ['--warn-text', '--warn-rgb', 0.16],
+ ['--danger-text', '--danger-rgb', 0.14], ['--danger-text', '--danger-rgb', 0.22],
+ ['--ok', '--ok-rgb', 0.14], ['--text-strong', '--accent-rgb', 0.3],
+ ['--text', '--sel-rgb', 0.22], ['--text', '--fg-rgb', 0.08]].forEach(([fg, rgb, a]) => {
+ ['--bg2', '--card', '--card2'].forEach((base) => {
+ out.push({ fg, on: { tint: rgb, alpha: a, base }, min: 'text', why: fg + ' / ' + rgb + ' ' + a + ' / ' + base });
+ });
+ });
+ // Dolu düğmeler
+ text('--on-accent', '--accent', 'birincil düğme');
+ text('--on-accent', { mix: ['--accent', '--accent-grad'] }, 'birincil düğme, geçişin ortası');
+ ui('--on-accent', '--accent-grad', 'birincil düğme, geçişin sonu');
+ text('--on-solid', '--danger', 'tehlikeli düğme, sessiz bayrağı');
+ text('--on-solid', '--lock', 'kilit bayrağı');
+ text('--on-warn', '--warn', 'uyarı bandı düğmesi');
+ text('--on-solo', '--solo', 'solo bayrağı');
+ // Kod düzenleyicileri
+ ['--code-fg', '--syn-kw', '--syn-type', '--syn-fn', '--syn-num', '--syn-com', '--syn-pre'].forEach((fg) => {
+ text(fg, '--code-bg', 'kod ' + fg);
+ });
+ text('--muted2', '--code-gutter', 'satır numaraları');
+ // Zaman çizelgesi cetvelindeki tempo ve işaret etiketleri
+ ['--card', '--card2'].forEach((base) => {
+ out.push({ fg: '--tempo', on: { tint: '--fg-rgb', alpha: 0.04, base }, min: 'text', why: 'tempo etiketi / ' + base });
+ out.push({ fg: '--cue-armed', on: { tint: '--fg-rgb', alpha: 0.04, base }, min: 'text', why: 'işaret etiketi / ' + base });
+ });
+ // Medya önizlemesinin üstündeki rozet ve düğmeler (görüntünün koyu perdesinde)
+ out.push({ fg: '--on-media-rgb', on: { scrim: '--media-scrim', base: '--media-bg' }, min: 'text', why: 'önizleme rozeti' });
+ // Marka yazısı
+ text('--brand-ink', '--bg', 'yazı işareti');
+ text('--brand-ink', '--bg2', 'yazı işareti');
+ // Yazı olmayan ama anlam taşıyan
+ SURFACES.forEach((s) => ui('--accent', s, 'odak halkası ve seçili kenar / ' + s));
+ ui('--knob', '--accent', 'açık anahtarın topuzu');
+ ui('--muted', '--card3', 'kapalı anahtarın topuzu');
+ ui('--cue-live', '--card', 'çalan klip kenarı');
+ ui('--cue-armed', '--card', 'sıradaki klip kenarı');
+ return out;
+ }
+
+ // Yüksek karşıtlıkta çizgiler de görünmeli
+ function aaaExtras() {
+ return [
+ { fg: '--line', on: '--card', min: UI, why: 'kart kenarı' },
+ { fg: '--line2', on: '--card', min: TEXT, why: 'denetim kenarı' },
+ { fg: '--line2', on: '--card3', min: UI, why: 'denetim kenarı / üstü' },
+ ];
+ }
+
+ function background(theme, on) {
+ if (typeof on === 'string') return T.solid(theme, on);
+ if (on.scrim) return T.solid(theme, on.scrim, T.solid(theme, on.base));
+ if (on.mix) {
+ const a = T.solid(theme, on.mix[0]);
+ const b = T.solid(theme, on.mix[1]);
+ return [0, 1, 2].map((i) => (a[i] + b[i]) / 2).concat(1);
+ }
+ return T.tint(theme, on.tint, on.alpha, on.base);
+ }
+
+ // Bir temanın bütün kurallarını ölçer; eşik altındakileri döndürür
+ function check(theme) {
+ const list = rules().concat(theme.aaa ? aaaExtras() : []);
+ const fails = [];
+ const measured = [];
+ list.forEach((r) => {
+ const bg = background(theme, r.on);
+ const fg = T.solid(theme, r.fg, bg);
+ const ratio = T.contrast(fg, bg);
+ const min = r.min === 'text' ? (theme.aaa ? AAA : TEXT) : r.min;
+ measured.push({ rule: r, ratio, min });
+ if (!(ratio >= min)) fails.push({ theme: theme.id, why: r.why, fg: r.fg, ratio: Math.round(ratio * 100) / 100, min });
+ });
+ return { fails, measured };
+ }
+
+ const api = { SURFACES, TEXT, AAA, UI, rules, check };
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
+ if (typeof window !== 'undefined') window.SVUiThemeContrast = api;
+})();
diff --git a/src/shared/ui-themes.js b/src/shared/ui-themes.js
new file mode 100644
index 00000000..adc7330f
--- /dev/null
+++ b/src/shared/ui-themes.js
@@ -0,0 +1,337 @@
+/*
+ * SoundVisualizer — CaYaDev Ses Görselleştirici
+ * Copyright (c) 2026 Çağan Turgut (CaYatur) — CaYaDev, https://cayadev.com
+ * https://github.com/CaYatur/SoundVisualizer
+ * SPDX-License-Identifier: MIT
+ */
+'use strict';
+/* ARAYÜZ TEMALARI (#770).
+
+ Panelin bütün renkleri bu tablodan gelir. admin.css ve studio.css renk
+ yazmaz; yalnız buradaki değişkenleri (`var(--x)`) kullanır. CaYaDev teması
+ admin.css'in :root bloğunda da aynen durur (betik yüklenmeden önceki ilk
+ çizim için); test ikisinin eşit olduğunu denetler.
+
+ Değişken türleri:
+ - renk: '#rrggbb' ya da 'rgba(...)' (popover, perde gibi yarı saydam
+ yüzeyler);
+ - kanal: '255 255 255' — CSS'te `rgb(var(--fg-rgb) / .05)` biçiminde
+ saydamlıkla kullanılır. Koyu temada beyaz olan bir ince örtü aydınlık
+ temada koyu olmalı; saydamlık CSS'te kalır, ton temadan gelir.
+
+ Görselleştirici çıktısı, yayın katmanı ve kullanıcının kendi renkleri
+ (paletler, renk seçiciler, klip renkleri) temaya bağlı DEĞİL. Medya
+ önizlemesinin zemini (`--media-*`) her temada koyu kalır: üstünde
+ görüntü durur.
+
+ "Cihaz varsayılanı" bir tema değil, seçim: işletim sisteminin açık/koyu
+ tercihine göre Karanlık ya da Aydınlık'a çözülür ve tercih değişince
+ canlı izlenir.
+
+ `--accent2` yazı olarak da kullanılıyor (grup başlıkları), bu yüzden koyu
+ zeminde açık olmalı; dolu düğmelerin geçişi `--accent` → `--accent-grad`
+ ve üstündeki yazı (`--on-accent`) geçişin ortasında da okunmalı. İkisi
+ aynı renk olamaz: koyu kartta 4.5:1 okunan bir kırmızının üstünde beyaz
+ yazı 4.5:1 okunamaz. */
+(function () {
+ const DEFAULT_ID = 'cayadev';
+ const SYSTEM_ID = 'system';
+
+ /* Bütün temaların tanımlaması ZORUNLU değişkenler ve rolleri. Rol,
+ karşıtlık testinin ve yeni bir değişken ekleyenin rehberi. */
+ const TOKENS = {
+ // Yüzeyler (koyudan açığa ya da açıktan koyuya, iç içe kartlar)
+ '--bg': 'color', '--bg2': 'color', '--surface': 'color',
+ '--card': 'color', '--card2': 'color', '--card3': 'color', '--card-end': 'color',
+ '--popover': 'color', '--scrim': 'color',
+ '--glow-a': 'color', '--glow-b': 'color',
+ // Çizgiler
+ '--line': 'color', '--line2': 'color',
+ // Yazı
+ '--text': 'color', '--text-strong': 'color', '--muted': 'color', '--muted2': 'color',
+ '--brand-ink': 'color',
+ // Vurgu
+ '--accent': 'color', '--accent2': 'color', '--accent-grad': 'color', '--accent-rgb': 'rgb', '--on-accent': 'color',
+ '--accent-text': 'color', '--accent-label': 'color', '--on-solid': 'color',
+ // Durumlar
+ '--ok': 'color', '--ok-rgb': 'rgb', '--ok-text': 'color',
+ '--warn': 'color', '--warn-rgb': 'rgb', '--warn-text': 'color', '--on-warn': 'color',
+ '--danger': 'color', '--danger-rgb': 'rgb', '--danger-text': 'color',
+ '--solo': 'color', '--on-solo': 'color', '--lock': 'color', '--off': 'color',
+ '--sel-rgb': 'rgb', '--info-rgb': 'rgb', '--tempo': 'color',
+ // Örtüler ve gölge
+ '--fg-rgb': 'rgb', '--shade-rgb': 'rgb', '--shadow-rgb': 'rgb', '--knob': 'color',
+ // Klip destesi, performans görünümü, ölçerler
+ '--cue-live': 'color', '--cue-armed': 'color', '--slot-default': 'color',
+ '--meter-mid': 'color', '--meter-hot': 'color',
+ // Kod düzenleyicileri (Studio, MilkDrop, şarkı sözü, MCP)
+ '--code-bg': 'color', '--code-gutter': 'color', '--code-fg': 'color',
+ '--syn-kw': 'color', '--syn-type': 'color', '--syn-fn': 'color',
+ '--syn-num': 'color', '--syn-com': 'color', '--syn-pre': 'color',
+ // Medya önizlemesi (her temada koyu)
+ '--media-bg': 'color', '--media-scrim': 'color', '--on-media-rgb': 'rgb',
+ };
+ const TOKEN_NAMES = Object.keys(TOKENS);
+
+ // Her temada aynı kalan medya zemini
+ const MEDIA = {
+ '--media-bg': '#05040a',
+ '--media-scrim': 'rgba(0, 0, 0, .55)',
+ '--on-media-rgb': '255 255 255',
+ };
+
+ const CAYADEV = Object.assign({
+ '--bg': '#0b0910', '--bg2': '#120d13', '--surface': '#17111a',
+ '--card': '#1b141f', '--card2': '#241a29', '--card3': '#2c2032', '--card-end': 'rgba(16, 11, 18, .9)',
+ '--popover': 'rgba(20, 14, 22, .985)', '--scrim': 'rgba(3, 3, 8, .62)',
+ '--glow-a': '#3d1016', '--glow-b': '#1d1030',
+ '--line': '#34222f', '--line2': '#472c3a',
+ '--text': '#f4edf1', '--text-strong': '#ffffff', '--muted': '#b2969f', '--muted2': '#a1858f',
+ '--brand-ink': '#ffffff',
+ '--accent': '#e11d2a', '--accent2': '#ff4d5e', '--accent-grad': '#ea2636', '--accent-rgb': '225 29 42', '--on-accent': '#ffffff',
+ '--accent-text': '#ff9aa3', '--accent-label': '#ffc7cd', '--on-solid': '#ffffff',
+ '--ok': '#43d98a', '--ok-rgb': '67 217 138', '--ok-text': '#9df0c4',
+ '--warn': '#ffc44d', '--warn-rgb': '255 196 77', '--warn-text': '#f3cf82', '--on-warn': '#1b141f',
+ '--danger': '#d93a40', '--danger-rgb': '255 80 80', '--danger-text': '#ff8a8a',
+ '--solo': '#f5a524', '--on-solo': '#1a1a1a', '--lock': '#6e56cf', '--off': '#6a4a52',
+ '--sel-rgb': '124 92 255', '--info-rgb': '120 180 255', '--tempo': '#50d2e6',
+ '--fg-rgb': '255 255 255', '--shade-rgb': '0 0 0', '--shadow-rgb': '0 0 0', '--knob': '#ffffff',
+ '--cue-live': '#2ea043', '--cue-armed': '#f0b429', '--slot-default': '#4f8cff',
+ '--meter-mid': '#ffd54a', '--meter-hot': '#ff5e6c',
+ '--code-bg': '#0d0a11', '--code-gutter': '#0a0810', '--code-fg': '#e7dfe9',
+ '--syn-kw': '#ff7b8a', '--syn-type': '#7fd6ff', '--syn-fn': '#c39bff',
+ '--syn-num': '#ffc46b', '--syn-com': '#9a8aa0', '--syn-pre': '#8fdca0',
+ }, MEDIA);
+
+ // Koyu temaların ortak durum ve kod renkleri CaYaDev'den
+ function darkFrom(over) { return Object.assign({}, CAYADEV, over); }
+
+ const LIGHT = Object.assign({
+ '--bg': '#eef0f4', '--bg2': '#e6e9ee', '--surface': '#f4f5f8',
+ '--card': '#ffffff', '--card2': '#f4f5f8', '--card3': '#e8ebf0', '--card-end': 'rgba(249, 250, 252, .96)',
+ '--popover': 'rgba(255, 255, 255, .99)', '--scrim': 'rgba(24, 28, 40, .42)',
+ '--glow-a': '#fbe3e6', '--glow-b': '#e3e7fb',
+ '--line': '#d8dce4', '--line2': '#bfc5d0',
+ '--text': '#16181d', '--text-strong': '#000000', '--muted': '#454b58', '--muted2': '#5b6170',
+ '--brand-ink': '#16181d',
+ '--accent': '#c8102e', '--accent2': '#b3132b', '--accent-grad': '#e0213d', '--accent-rgb': '200 16 46', '--on-accent': '#ffffff',
+ '--accent-text': '#a00d24', '--accent-label': '#8f0c20', '--on-solid': '#ffffff',
+ '--ok': '#0e6236', '--ok-rgb': '14 98 54', '--ok-text': '#0d5e33',
+ '--warn': '#8f5600', '--warn-rgb': '200 130 0', '--warn-text': '#6f4300', '--on-warn': '#ffffff',
+ '--danger': '#c62828', '--danger-rgb': '210 40 40', '--danger-text': '#9c1a17',
+ '--solo': '#f5a524', '--on-solo': '#1a1a1a', '--lock': '#5b45c2', '--off': '#9aa0ad',
+ '--sel-rgb': '98 70 230', '--info-rgb': '0 112 200', '--tempo': '#00708a',
+ '--fg-rgb': '20 24 36', '--shade-rgb': '255 255 255', '--shadow-rgb': '30 36 52', '--knob': '#ffffff',
+ '--cue-live': '#1f883d', '--cue-armed': '#875c00', '--slot-default': '#2f6fe0',
+ '--meter-mid': '#d9a400', '--meter-hot': '#e5484d',
+ '--code-bg': '#f6f7f9', '--code-gutter': '#eceef2', '--code-fg': '#1f2328',
+ '--syn-kw': '#b3132b', '--syn-type': '#0b5fa5', '--syn-fn': '#6b33b8',
+ '--syn-num': '#8a5100', '--syn-com': '#5d6472', '--syn-pre': '#1a6e33',
+ }, MEDIA);
+
+ const THEMES = [
+ { id: 'cayadev', name: 'CaYaDev', scheme: 'dark', tokens: CAYADEV },
+ {
+ id: 'dark', name: 'Karanlık', scheme: 'dark',
+ tokens: darkFrom({
+ '--bg': '#0f1115', '--bg2': '#14171c', '--surface': '#181b21',
+ '--card': '#1c1f26', '--card2': '#21252c', '--card3': '#272b33', '--card-end': 'rgba(21, 24, 29, .92)',
+ '--popover': 'rgba(24, 27, 33, .985)', '--scrim': 'rgba(4, 5, 8, .62)',
+ '--glow-a': '#14223a', '--glow-b': '#191b26',
+ '--line': '#2f343d', '--line2': '#3e4450',
+ '--text': '#e9ecf1', '--muted': '#aab1bd', '--muted2': '#9299a6',
+ '--accent': '#3170e2', '--accent2': '#6aa7ff', '--accent-grad': '#2a5fd0', '--accent-rgb': '49 112 226',
+ '--accent-text': '#9cc4ff', '--accent-label': '#c7dcff',
+ '--off': '#4b515c',
+ '--code-bg': '#111317', '--code-gutter': '#0d0f12', '--code-fg': '#e6e8ec',
+ '--syn-com': '#8f97a3',
+ }),
+ },
+ { id: 'light', name: 'Aydınlık', scheme: 'light', tokens: LIGHT },
+ {
+ id: 'contrast', name: 'Yüksek Karşıtlık', scheme: 'dark', aaa: true,
+ tokens: darkFrom({
+ '--bg': '#000000', '--bg2': '#000000', '--surface': '#000000',
+ '--card': '#000000', '--card2': '#0c0c0c', '--card3': '#1a1a1a', '--card-end': 'rgba(0, 0, 0, 1)',
+ '--popover': 'rgba(0, 0, 0, 1)', '--scrim': 'rgba(0, 0, 0, .82)',
+ '--glow-a': '#000000', '--glow-b': '#000000',
+ '--line': '#8c8c8c', '--line2': '#d0d0d0',
+ '--text': '#ffffff', '--muted': '#ececec', '--muted2': '#d4d4d4',
+ '--accent': '#ffd400', '--accent2': '#ffe14d', '--accent-grad': '#ffe14d', '--accent-rgb': '255 212 0', '--on-accent': '#000000',
+ '--on-solid': '#000000', '--knob': '#000000',
+ '--accent-text': '#ffe14d', '--accent-label': '#fff1a6',
+ '--ok': '#3dff8f', '--ok-rgb': '61 255 143', '--ok-text': '#9dffc7',
+ '--warn': '#ffae33', '--warn-rgb': '255 174 51', '--warn-text': '#ffd08a', '--on-warn': '#000000',
+ '--danger': '#ff7070', '--danger-rgb': '255 112 112', '--danger-text': '#ffa3a3',
+ '--solo': '#ffae33', '--on-solo': '#000000', '--lock': '#c7b3ff', '--off': '#3d3d3d',
+ '--sel-rgb': '170 150 255', '--info-rgb': '90 220 255', '--tempo': '#5ae6ff',
+ '--cue-live': '#3dff8f', '--cue-armed': '#ffd400', '--slot-default': '#5fb0ff',
+ '--meter-mid': '#ffd400', '--meter-hot': '#ff5c5c',
+ '--code-bg': '#000000', '--code-gutter': '#0c0c0c', '--code-fg': '#ffffff',
+ '--syn-kw': '#ff9eb0', '--syn-type': '#8ae4ff', '--syn-fn': '#dcc6ff',
+ '--syn-num': '#ffd78a', '--syn-com': '#cfcfcf', '--syn-pre': '#a6f5ba',
+ }),
+ },
+ {
+ id: 'midnight', name: 'Gece Mavisi', scheme: 'dark',
+ tokens: darkFrom({
+ '--bg': '#0a0f1c', '--bg2': '#0e1424', '--surface': '#111829',
+ '--card': '#141c30', '--card2': '#1a2440', '--card3': '#212d4d', '--card-end': 'rgba(13, 19, 34, .92)',
+ '--popover': 'rgba(17, 24, 41, .985)', '--scrim': 'rgba(3, 6, 14, .64)',
+ '--glow-a': '#0c2b4a', '--glow-b': '#1b1640',
+ '--line': '#24304d', '--line2': '#32426c',
+ '--text': '#e6edf8', '--muted': '#a6b4ce', '--muted2': '#8e9dbb',
+ '--accent': '#22c3d8', '--accent2': '#67e1ef', '--accent-grad': '#67e1ef', '--accent-rgb': '34 195 216', '--on-accent': '#04121a',
+ '--knob': '#04121a',
+ '--accent-text': '#8fe9f4', '--accent-label': '#c4f3f9',
+ '--off': '#3f4c6b',
+ '--code-bg': '#0b1222', '--code-gutter': '#08101d', '--code-fg': '#dde6f5',
+ '--syn-com': '#8d9bb8',
+ }),
+ },
+ {
+ id: 'sepia', name: 'Sepya', scheme: 'light',
+ tokens: Object.assign({}, LIGHT, {
+ '--bg': '#efe5d3', '--bg2': '#e8dcc6', '--surface': '#f4ebdc',
+ '--card': '#fbf5ea', '--card2': '#f3e9d7', '--card3': '#e9dbc2', '--card-end': 'rgba(246, 238, 224, .96)',
+ '--popover': 'rgba(251, 245, 234, .99)', '--scrim': 'rgba(50, 35, 20, .42)',
+ '--glow-a': '#f4d6bf', '--glow-b': '#e8dcc4',
+ '--line': '#dac7a5', '--line2': '#c6af88',
+ '--text': '#3a2a1a', '--text-strong': '#22160a', '--muted': '#5a4633', '--muted2': '#6a5541',
+ '--brand-ink': '#3a2a1a',
+ '--accent': '#a8441a', '--accent2': '#963c16', '--accent-grad': '#c25a22', '--accent-rgb': '168 68 26',
+ '--accent-text': '#82300f', '--accent-label': '#762c0f',
+ '--ok': '#1f5a33', '--ok-rgb': '31 90 51', '--ok-text': '#1d5530',
+ '--warn': '#875400', '--warn-text': '#674000',
+ '--danger': '#b3261e', '--danger-text': '#8a1a14',
+ '--fg-rgb': '60 40 20', '--shade-rgb': '255 250 240', '--shadow-rgb': '80 55 30',
+ '--off': '#a8987f', '--tempo': '#00627a',
+ '--cue-live': '#2c7040', '--cue-armed': '#7a4f00', '--slot-default': '#3b6db0',
+ '--code-bg': '#f8f0e2', '--code-gutter': '#efe3cf', '--code-fg': '#3a2a1a',
+ '--syn-kw': '#9e2f1a', '--syn-type': '#1d5986', '--syn-fn': '#663b98',
+ '--syn-num': '#7f5200', '--syn-com': '#66584a', '--syn-pre': '#2a6530',
+ }),
+ },
+ {
+ id: 'neon', name: 'Neon', scheme: 'dark',
+ tokens: darkFrom({
+ '--bg': '#0d0618', '--bg2': '#120a22', '--surface': '#160c2a',
+ '--card': '#1a0f31', '--card2': '#23143f', '--card3': '#2c1a4e', '--card-end': 'rgba(18, 9, 34, .92)',
+ '--popover': 'rgba(24, 13, 44, .985)', '--scrim': 'rgba(6, 2, 14, .64)',
+ '--glow-a': '#4a0c4f', '--glow-b': '#0a2c4a',
+ '--line': '#3a2363', '--line2': '#4f3085',
+ '--text': '#f6ecff', '--muted': '#c6b2e2', '--muted2': '#a993cc',
+ '--accent': '#d61f87', '--accent2': '#2fd8f0', '--accent-grad': '#9b2ff0', '--accent-rgb': '214 31 135',
+ '--accent-text': '#ff9fd6', '--accent-label': '#ffcbe9',
+ '--off': '#5a3f80',
+ '--code-bg': '#110820', '--code-gutter': '#0c0618', '--code-fg': '#efe4ff',
+ '--syn-com': '#a08fbe',
+ }),
+ },
+ ];
+
+ const BY_ID = {};
+ THEMES.forEach((t) => { BY_ID[t.id] = t; });
+
+ /* Seçim listesi: temalar + "Cihaz varsayılanı". Sıra panelde görünen
+ sıra. */
+ const CHOICES = [
+ { id: 'cayadev' }, { id: SYSTEM_ID, name: 'Cihaz varsayılanı' },
+ { id: 'dark' }, { id: 'light' }, { id: 'contrast' },
+ { id: 'midnight' }, { id: 'sepia' }, { id: 'neon' },
+ ].map((c) => (c.id === SYSTEM_ID ? c : { id: c.id, name: BY_ID[c.id].name }));
+
+ function normalizeId(id) {
+ return id === SYSTEM_ID || Object.prototype.hasOwnProperty.call(BY_ID, id) ? id : DEFAULT_ID;
+ }
+
+ // Seçimi (system dahil) somut bir temaya çevirir
+ function resolve(id, prefersDark) {
+ const n = normalizeId(id);
+ if (n === SYSTEM_ID) return BY_ID[prefersDark === false ? 'light' : 'dark'];
+ return BY_ID[n];
+ }
+
+ // ---- Renk hesapları (karşıtlık testi ve öz testteki tarama kullanır) ----
+ function parseColor(s) {
+ if (typeof s !== 'string') return null;
+ const t = s.trim();
+ let m = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(t);
+ if (m) {
+ let h = m[1];
+ if (h.length === 3) h = h.split('').map((c) => c + c).join('');
+ const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), a];
+ }
+ m = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/i.exec(t);
+ if (m) {
+ let a = m[4] === undefined ? 1 : m[4].endsWith('%') ? parseFloat(m[4]) / 100 : parseFloat(m[4]);
+ if (!Number.isFinite(a)) a = 1;
+ return [+m[1], +m[2], +m[3], a];
+ }
+ m = /^([\d.]+)\s+([\d.]+)\s+([\d.]+)$/.exec(t); // kanal değişkeni
+ if (m) return [+m[1], +m[2], +m[3], 1];
+ return null;
+ }
+
+ // Yarı saydam bir rengi opak bir zeminin üstüne koyar
+ function over(top, base) {
+ const a = top[3];
+ return [0, 1, 2].map((i) => top[i] * a + base[i] * (1 - a)).concat(1);
+ }
+
+ function luminance(c) {
+ const ch = c.slice(0, 3).map((v) => {
+ const x = v / 255;
+ return x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
+ });
+ return 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2];
+ }
+
+ function contrast(a, b) {
+ const ca = typeof a === 'string' ? parseColor(a) : a;
+ const cb = typeof b === 'string' ? parseColor(b) : b;
+ const la = luminance(ca);
+ const lb = luminance(cb);
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
+ }
+
+ // Bir temanın değişkenini opak renge çevirir (yarı saydamsa --bg üstünde)
+ function solid(theme, name, base) {
+ const c = parseColor(theme.tokens[name]);
+ if (!c) return null;
+ if (c[3] >= 1) return c;
+ return over(c, base || solid(theme, '--bg'));
+ }
+
+ // Kanal değişkenini saydamlıkla bir zeminin üstüne koyar
+ function tint(theme, rgbName, alpha, baseName) {
+ const c = parseColor(theme.tokens[rgbName]);
+ return over([c[0], c[1], c[2], alpha], solid(theme, baseName));
+ }
+
+ /* Belgeye uygular. CaYaDev'de satır içi değerler silinir: admin.css'in
+ :root'u zaten CaYaDev. */
+ function apply(doc, id, prefersDark) {
+ const theme = resolve(id, prefersDark);
+ const el = doc.documentElement;
+ TOKEN_NAMES.forEach((n) => {
+ if (theme.id === DEFAULT_ID) el.style.removeProperty(n);
+ else el.style.setProperty(n, theme.tokens[n]);
+ });
+ el.style.colorScheme = theme.scheme;
+ el.setAttribute('data-ui-theme', theme.id);
+ el.setAttribute('data-ui-scheme', theme.scheme);
+ return theme;
+ }
+
+ const api = {
+ DEFAULT_ID, SYSTEM_ID, TOKENS, TOKEN_NAMES, THEMES, CHOICES,
+ byId: (id) => BY_ID[id] || null,
+ normalizeId, resolve, apply,
+ parseColor, over, luminance, contrast, solid, tint,
+ };
+
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
+ if (typeof window !== 'undefined') window.SVUiThemes = api;
+})();
diff --git a/tests/helpers/theme-scan.js b/tests/helpers/theme-scan.js
new file mode 100644
index 00000000..68aa79cc
--- /dev/null
+++ b/tests/helpers/theme-scan.js
@@ -0,0 +1,158 @@
+/*
+ * SoundVisualizer — CaYaDev Ses Görselleştirici
+ * Copyright (c) 2026 Çağan Turgut (CaYatur) — CaYaDev, https://cayadev.com
+ * https://github.com/CaYatur/SoundVisualizer
+ * SPDX-License-Identifier: MIT
+ */
+'use strict';
+/* Tema testlerinin CSS ve JS tarayıcısı (#770). Renk sabitlerini bulur;
+ yorumları ve url(...) içini atlar (veri URI'leri görüntü). */
+
+// Onaltılık renk, sayıyla başlayan rgb()/rgba()/hsl()/hsla()
+const COLOR = /#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?)\(\s*[\d.]/g;
+
+// CSS'te değer olarak yazılabilecek adlı renkler (transparent ve currentColor serbest)
+const NAMED = ['white', 'black', 'red', 'green', 'blue', 'yellow', 'orange', 'gray', 'grey',
+ 'silver', 'purple', 'pink', 'magenta', 'cyan', 'lime', 'navy', 'teal', 'maroon', 'olive',
+ 'aqua', 'fuchsia', 'gold', 'brown', 'crimson', 'coral', 'salmon', 'tomato', 'violet',
+ 'indigo', 'khaki', 'beige', 'ivory', 'lavender', 'orchid', 'plum', 'tan', 'turquoise',
+ 'wheat', 'snow', 'azure', 'chartreuse', 'darkred', 'darkgray', 'darkgrey', 'lightgray',
+ 'lightgrey', 'whitesmoke', 'gainsboro', 'dimgray', 'dimgrey'];
+const NAMED_RE = new RegExp('(^|[\\s,(])(' + NAMED.join('|') + ')(?=$|[\\s,;)!])', 'i');
+
+function blank(s, re) {
+ return s.replace(re, (m) => m.replace(/[^\n]/g, ' '));
+}
+
+function stripCss(src) {
+ let s = blank(src, /\/\*[\s\S]*?\*\//g);
+ s = blank(s, /url\((?:"[^"]*"|'[^']*'|[^)]*)\)/g);
+ return s;
+}
+
+function lineOf(src, idx) {
+ return src.slice(0, idx).split('\n').length;
+}
+
+/* Kurallar: { sel, body, start, decls: [{ prop, value, index }] }. @media
+ ve @keyframes içindeki kurallar da düz kural olarak gelir. */
+function cssRules(src) {
+ const s = stripCss(src);
+ const out = [];
+ const re = /([^{}]*)\{([^{}]*)\}/g;
+ let m;
+ while ((m = re.exec(s))) {
+ const sel = m[1].replace(/\s+/g, ' ').trim();
+ const body = m[2];
+ const decls = [];
+ let off = m.index + m[1].length + 1;
+ body.split(';').forEach((d) => {
+ const i = d.indexOf(':');
+ if (i > 0) decls.push({ prop: d.slice(0, i).trim(), value: d.slice(i + 1).trim(), index: off });
+ off += d.length + 1;
+ });
+ out.push({ sel, body, start: m.index, decls });
+ }
+ return out;
+}
+
+// :root bloğundaki özel özellikler
+function rootVars(src) {
+ const r = cssRules(src).find((x) => x.sel === ':root');
+ const map = {};
+ if (r) r.decls.forEach((d) => { if (d.prop.startsWith('--')) map[d.prop] = d.value; });
+ return map;
+}
+
+// :root dışındaki renk sabitleri
+function cssLiterals(src) {
+ const found = [];
+ cssRules(src).forEach((r) => {
+ if (r.sel === ':root') return;
+ r.decls.forEach((d) => {
+ const v = d.value;
+ let m;
+ COLOR.lastIndex = 0;
+ while ((m = COLOR.exec(v))) {
+ found.push({ line: lineOf(src, d.index), sel: r.sel, prop: d.prop, literal: m[0] });
+ }
+ const n = NAMED_RE.exec(v);
+ if (n && !/^font|^grid-template|^animation|^transition|^content$/.test(d.prop)) {
+ found.push({ line: lineOf(src, d.index), sel: r.sel, prop: d.prop, literal: n[2] });
+ }
+ });
+ });
+ return found;
+}
+
+// Kullanılan değişkenler ve kullanım biçimleri
+function cssVarUses(src) {
+ const s = stripCss(src);
+ const uses = [];
+ const re = /var\(\s*(--[\w-]+)/g;
+ let m;
+ while ((m = re.exec(s))) uses.push({ name: m[1], line: lineOf(src, m.index) });
+ return uses;
+}
+
+// Kurallarda tanımlanan özel özellikler (ör. .cd-cell { --slot: ... })
+function cssLocalVars(src) {
+ const set = new Set();
+ cssRules(src).forEach((r) => r.decls.forEach((d) => { if (d.prop.startsWith('--')) set.add(d.prop); }));
+ return set;
+}
+
+// rgb(var(--x) / a) biçimindeki kanal kullanımları
+function cssChannelUses(src) {
+ const s = stripCss(src);
+ const out = [];
+ const re = /rgba?\(\s*var\(\s*(--[\w-]+)\s*\)/g;
+ let m;
+ while ((m = re.exec(s))) out.push({ name: m[1], line: lineOf(src, m.index) });
+ return out;
+}
+
+/* JS: dize sabitlerinin içindeki renkler (yorumlar sayılmaz). Basit bir
+ tarayıcı: dizeleri, yorumları ve düzenli ifadeleri ayırt eder. */
+function jsStringColors(src) {
+ const found = [];
+ let i = 0;
+ const n = src.length;
+ let lastSig = '';
+ while (i < n) {
+ const c = src[i];
+ const d = src[i + 1];
+ if (c === '/' && d === '/') { const e = src.indexOf('\n', i); i = e < 0 ? n : e; continue; }
+ if (c === '/' && d === '*') { const e = src.indexOf('*/', i + 2); i = e < 0 ? n : e + 2; continue; }
+ if (c === '\'' || c === '"' || c === '`') {
+ let j = i + 1;
+ while (j < n && src[j] !== c) { if (src[j] === '\\') j++; j++; }
+ const str = src.slice(i + 1, j);
+ let m;
+ COLOR.lastIndex = 0;
+ while ((m = COLOR.exec(str))) found.push({ line: lineOf(src, i), literal: m[0], text: str.slice(0, 80) });
+ i = j + 1;
+ lastSig = 'x';
+ continue;
+ }
+ // Düzenli ifade sabiti: bölme işaretinden sonra gelen bir ifade değilse
+ if (c === '/' && !/[\w)\]$]/.test(lastSig)) {
+ let j = i + 1;
+ let cls = false;
+ while (j < n && (src[j] !== '/' || cls) && src[j] !== '\n') {
+ if (src[j] === '\\') j++;
+ else if (src[j] === '[') cls = true;
+ else if (src[j] === ']') cls = false;
+ j++;
+ }
+ i = j + 1;
+ lastSig = 'x';
+ continue;
+ }
+ if (!/\s/.test(c)) lastSig = c;
+ i++;
+ }
+ return found;
+}
+
+module.exports = { COLOR, NAMED, stripCss, cssRules, rootVars, cssLiterals, cssVarUses, cssLocalVars, cssChannelUses, jsStringColors };
diff --git a/tests/logo-lib-ui.test.js b/tests/logo-lib-ui.test.js
index cabcba12..f83a5df0 100644
--- a/tests/logo-lib-ui.test.js
+++ b/tests/logo-lib-ui.test.js
@@ -104,8 +104,9 @@ test('sil düğmesi çarpı çizer, dosyayı siler ve seçimi bırakır', async
const css = fs.readFileSync(path.join(__dirname, '..', 'src', 'admin', 'admin.css'), 'utf8');
assert.match(css, /\.logo-lib-del \{[\s\S]*z-index:\s*3/);
- assert.match(css, /\.logo-lib-del \.svi \{[^}]*color:\s*#fff/);
- assert.match(css, /\.logo-lib-del:hover,\s*\n\.logo-lib-del:focus-visible \{[^}]*background:\s*#e23b3b/);
+ // Renkler temadan (#770): görüntünün üstünde beyaz, üzerine gelince tehlike rengi
+ assert.match(css, /\.logo-lib-del \.svi \{[^}]*color:\s*rgb\(var\(--on-media-rgb\)\)/);
+ assert.match(css, /\.logo-lib-del:hover,\s*\n\.logo-lib-del:focus-visible \{[^}]*background:\s*var\(--danger\)/);
window.api.mediaLibList = async () => [{ id: 'v1', name: 'Klip', url: 'sv-media://local/x', kind: 'video' }];
const media = window.SVMediaLibUi.mount({ selectedId: 'v1' });
diff --git a/tests/ui-themes.test.js b/tests/ui-themes.test.js
new file mode 100644
index 00000000..364da826
--- /dev/null
+++ b/tests/ui-themes.test.js
@@ -0,0 +1,492 @@
+/*
+ * SoundVisualizer — CaYaDev Ses Görselleştirici
+ * Copyright (c) 2026 Çağan Turgut (CaYatur) — CaYaDev, https://cayadev.com
+ * https://github.com/CaYatur/SoundVisualizer
+ * SPDX-License-Identifier: MIT
+ */
+'use strict';
+/* ARAYÜZ TEMALARI (#770).
+
+ Bu testler yeni bir panel eklenince DÜŞMEK için var:
+ - admin.css / studio.css'e :root dışında bir renk yazılırsa,
+ - tanımsız bir değişken kullanılırsa,
+ - panel betiğine (src/admin/*.js) yeni bir renk dizesi girerse,
+ - bir temada yazı ile zemini arasındaki karşıtlık eşiğin altına inerse,
+ - aynı kuralda yazı ve zemin olarak eşlenen iki değişken bir temada
+ okunmaz olursa.
+ Renk gerçekten kullanıcı içeriğiyse (palet varsayılanı, klip rengi) aşağıdaki
+ JS_CONTENT listesine gerekçesiyle girer. */
+const test = require('node:test');
+const assert = require('node:assert');
+const fs = require('fs');
+const path = require('path');
+const vm = require('vm');
+
+const T = require('../src/shared/ui-themes');
+const C = require('../src/shared/ui-theme-contrast');
+const S = require('./helpers/theme-scan');
+
+const root = path.join(__dirname, '..');
+const read = (p) => fs.readFileSync(path.join(root, p), 'utf8');
+const CSS_FILES = ['src/admin/admin.css', 'src/admin/studio.css'];
+
+// ---------------------------------------------------------------------------
+// Tablo
+// ---------------------------------------------------------------------------
+test('tema tablosu: istenen temalar ve seçimler var', () => {
+ const ids = T.THEMES.map((t) => t.id);
+ ['cayadev', 'dark', 'light', 'contrast'].forEach((id) => assert.ok(ids.includes(id), id));
+ assert.ok(ids.length >= 6, 'birkaç farklı tarz: ' + ids.join(', '));
+ assert.strictEqual(new Set(ids).size, ids.length, 'kimlikler tekil');
+ assert.strictEqual(T.DEFAULT_ID, 'cayadev');
+ const choices = T.CHOICES.map((c) => c.id);
+ assert.deepStrictEqual(choices.filter((c) => c !== T.SYSTEM_ID).sort(), ids.slice().sort(), 'her tema seçilebilir');
+ assert.ok(choices.includes(T.SYSTEM_ID), 'Cihaz varsayılanı seçilebilir');
+ assert.strictEqual(choices[0], 'cayadev', 'ilk seçenek varsayılan tema');
+ T.THEMES.forEach((t) => {
+ assert.ok(t.scheme === 'dark' || t.scheme === 'light', t.id + ' scheme');
+ assert.ok(typeof t.name === 'string' && t.name.trim(), t.id + ' adı');
+ });
+ assert.ok(T.THEMES.some((t) => t.scheme === 'light') && T.THEMES.some((t) => t.scheme === 'dark'));
+ assert.strictEqual(T.byId('contrast').aaa, true, 'Yüksek Karşıtlık AAA eşiğiyle ölçülür');
+});
+
+test('her tema bütün değişkenleri tanımlıyor, fazlasını değil', () => {
+ T.THEMES.forEach((t) => {
+ const keys = Object.keys(t.tokens).sort();
+ assert.deepStrictEqual(keys, T.TOKEN_NAMES.slice().sort(), t.id);
+ T.TOKEN_NAMES.forEach((n) => {
+ const v = t.tokens[n];
+ const c = T.parseColor(v);
+ assert.ok(c, t.id + ' ' + n + ' çözülemedi: ' + v);
+ if (T.TOKENS[n] === 'rgb') assert.match(v, /^\d{1,3} \d{1,3} \d{1,3}$/, t.id + ' ' + n + ' kanal biçimi');
+ else assert.match(v, /^#[0-9a-f]{6}$|^rgba\(/, t.id + ' ' + n + ' renk biçimi');
+ c.slice(0, 3).forEach((x) => assert.ok(x >= 0 && x <= 255, t.id + ' ' + n));
+ });
+ });
+});
+
+test('medya zemini her temada aynı ve koyu (üstünde görüntü durur)', () => {
+ const ref = T.byId('cayadev').tokens;
+ T.THEMES.forEach((t) => {
+ ['--media-bg', '--media-scrim', '--on-media-rgb'].forEach((n) => assert.strictEqual(t.tokens[n], ref[n], t.id + ' ' + n));
+ });
+ assert.ok(T.luminance(T.parseColor(ref['--media-bg'])) < 0.02);
+});
+
+test('aydınlık temalar aydınlık, koyu temalar koyu', () => {
+ T.THEMES.forEach((t) => {
+ const bg = T.luminance(T.solid(t, '--bg'));
+ const tx = T.luminance(T.solid(t, '--text'));
+ if (t.scheme === 'light') assert.ok(bg > tx, t.id + ': zemin yazıdan açık');
+ else assert.ok(bg < tx, t.id + ': zemin yazıdan koyu');
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Karşıtlık
+// ---------------------------------------------------------------------------
+test('karşıtlık: her temada her kural eşiğin üstünde', () => {
+ const all = [];
+ T.THEMES.forEach((t) => all.push(...C.check(t).fails));
+ assert.deepStrictEqual(all, [], all.map((f) => f.theme + ': ' + f.why + ' ' + f.ratio + ' < ' + f.min).join('\n'));
+});
+
+test('karşıtlık hesabı WCAG değerlerini veriyor', () => {
+ assert.strictEqual(Math.round(T.contrast('#000000', '#ffffff') * 100) / 100, 21);
+ assert.strictEqual(T.contrast('#777777', '#777777'), 1);
+ // WCAG örneği: #767676 beyazda 4.54
+ assert.strictEqual(Math.round(T.contrast('#767676', '#ffffff') * 100) / 100, 4.54);
+ // Yarı saydam beyaz siyahın üstünde gri olur
+ const g = T.over([255, 255, 255, 0.5], [0, 0, 0, 1]);
+ assert.deepStrictEqual(g.map(Math.round), [128, 128, 128, 1]);
+});
+
+test('karşıtlık kuralları: yazı, düğme, kod ve odak halkası kapsanıyor', () => {
+ const rules = C.rules();
+ const has = (fg, on) => rules.some((r) => r.fg === fg && (typeof r.on === 'string' ? r.on === on : true));
+ ['--text', '--muted', '--muted2'].forEach((fg) => C.SURFACES.forEach((s) => assert.ok(has(fg, s), fg + '/' + s)));
+ assert.ok(has('--on-accent', '--accent'));
+ assert.ok(rules.some((r) => r.on && r.on.mix), 'geçişin ortası ölçülüyor');
+ ['--syn-kw', '--syn-type', '--syn-fn', '--syn-num', '--syn-com', '--syn-pre', '--code-fg'].forEach((fg) => assert.ok(has(fg, '--code-bg'), fg));
+ assert.ok(rules.some((r) => r.fg === '--accent' && r.min === C.UI), 'odak halkası 3:1');
+ assert.strictEqual(C.TEXT, 4.5);
+ assert.strictEqual(C.AAA, 7);
+});
+
+/* Aynı kuralda yazı rengi ve düz zemin değişkenle verilmişse o ikili her
+ temada okunmalı. Yeni bir kart kendi zeminini ve yazısını seçtiğinde
+ bunu ayrıca listeye yazmak gerekmiyor. */
+test('CSS kuralları: aynı kuraldaki yazı ve zemin her temada okunuyor', () => {
+ const fails = [];
+ const solidVar = (v) => {
+ const m = /^var\((--[\w-]+)\)$/.exec(v.trim());
+ return m && T.TOKENS[m[1]] === 'color' ? m[1] : null;
+ };
+ const gradVars = (v) => {
+ const m = /^linear-gradient\(\s*[\d.]+deg\s*,\s*var\((--[\w-]+)\)\s*,\s*var\((--[\w-]+)\)\s*\)$/.exec(v.trim());
+ return m ? [m[1], m[2]] : null;
+ };
+ let pairs = 0;
+ CSS_FILES.forEach((file) => {
+ S.cssRules(read(file)).forEach((r) => {
+ const color = r.decls.find((d) => d.prop === 'color');
+ const bg = r.decls.find((d) => d.prop === 'background' || d.prop === 'background-color');
+ if (!color || !bg) return;
+ const fg = solidVar(color.value);
+ if (!fg || fg === '--media-bg') return;
+ const b = solidVar(bg.value);
+ const g = b ? null : gradVars(bg.value);
+ // Örtülü zemin: rgb(var(--x-rgb) / a), kartların ve rayın üstünde
+ const tm = b || g ? null : /^rgb\(var\((--[\w-]+)\) \/ ([\d.]+)\)$/.exec(bg.value.trim());
+ if (!b && !g && !tm) return;
+ pairs++;
+ T.THEMES.forEach((t) => {
+ const min = t.aaa ? C.AAA : C.TEXT;
+ const bases = [];
+ if (b) bases.push(T.solid(t, b));
+ else if (g) {
+ const a = T.solid(t, g[0]);
+ const c = T.solid(t, g[1]);
+ bases.push([0, 1, 2].map((i) => (a[i] + c[i]) / 2).concat(1));
+ } else {
+ ['--bg2', '--card', '--card2'].forEach((s) => bases.push(T.tint(t, tm[1], parseFloat(tm[2]), s)));
+ }
+ bases.forEach((base) => {
+ const ratio = T.contrast(T.solid(t, fg, base), base);
+ if (ratio < min) fails.push(t.id + ' ' + file + ' "' + r.sel + '" ' + fg + ' on ' + (b || (g && g.join('→')) || bg.value) + ' = ' + ratio.toFixed(2));
+ });
+ });
+ });
+ });
+ assert.ok(pairs >= 10, 'denetlenen ikili sayısı: ' + pairs);
+ assert.deepStrictEqual(fails, []);
+});
+
+// ---------------------------------------------------------------------------
+// CSS koruması
+// ---------------------------------------------------------------------------
+test('admin.css :root CaYaDev temasının aynısı', () => {
+ const vars = S.rootVars(read('src/admin/admin.css'));
+ const cay = T.byId('cayadev').tokens;
+ T.TOKEN_NAMES.forEach((n) => assert.strictEqual(vars[n], cay[n], n));
+ // :root'ta temanın dışında yalnız düzen ve türetilmiş değişkenler
+ const extra = Object.keys(vars).filter((n) => !(n in cay)).sort();
+ assert.deepStrictEqual(extra, ['--accent-soft', '--dock-w', '--radius', '--radius-sm', '--rail-w']);
+ assert.match(vars['--accent-soft'], /^rgb\(var\(--accent-rgb\) \/ [\d.]+\)$/);
+});
+
+CSS_FILES.forEach((file) => {
+ test(file + ': :root dışında renk sabiti yok', () => {
+ const lits = S.cssLiterals(read(file));
+ assert.deepStrictEqual(lits.map((l) => file + ':' + l.line + ' ' + l.sel + ' { ' + l.prop + ': ' + l.literal + ' }'), [],
+ 'renk yerine bir tema değişkeni kullanın (src/shared/ui-themes.js)');
+ });
+
+ test(file + ': kullanılan her değişken tanımlı', () => {
+ const local = new Set([...S.cssLocalVars(read('src/admin/admin.css')), ...S.cssLocalVars(read('src/admin/studio.css'))]);
+ // Betiğin satır içi yazdığı düzen değişkenleri (renk değil)
+ const fromJs = ['--cols'];
+ const known = new Set([...T.TOKEN_NAMES, ...Object.keys(S.rootVars(read('src/admin/admin.css'))), ...local, ...fromJs]);
+ const missing = S.cssVarUses(read(file)).filter((u) => !known.has(u.name));
+ assert.deepStrictEqual(missing.map((u) => file + ':' + u.line + ' ' + u.name), []);
+ });
+
+ test(file + ': rgb(var(--x) / a) yalnız kanal değişkenleriyle', () => {
+ const bad = S.cssChannelUses(read(file)).filter((u) => T.TOKENS[u.name] !== 'rgb');
+ assert.deepStrictEqual(bad.map((u) => file + ':' + u.line + ' ' + u.name), []);
+ });
+});
+
+test('tarayıcı: koruma gerçekten yakalıyor', () => {
+ const css = ':root { --bg: #000; }\n.a { color: #fff; }\n.b { background: rgba(0,0,0,.5); }\n' +
+ '.c { border: 1px solid white; }\n/* .d { color: #123456 } */\n.e { background: url("data:x,%23fff rgba(1,2,3,.4)"); }\n' +
+ '.f { background: rgb(var(--fg-rgb) / .1); color: var(--text); white-space: nowrap; }';
+ const lits = S.cssLiterals(css).map((l) => l.sel + ' ' + l.literal);
+ assert.deepStrictEqual(lits, ['.a #fff', '.b rgba(0', '.c white']);
+ const js = "const a = '#ff0000'; // '#00ff00'\n/* '#0000ff' */ const b = x / 2 / 3; const r = /#[0-9]/; const c = `rgba(1,2,3,.5)`;";
+ assert.deepStrictEqual(S.jsStringColors(js).map((x) => x.literal), ['#ff0000', 'rgba(1']);
+});
+
+// ---------------------------------------------------------------------------
+// Panel betikleri
+// ---------------------------------------------------------------------------
+/* Panel betiklerindeki renk dizeleri: hepsi KULLANICI İÇERİĞİ. Panelin kendi
+ rengi buraya girmez; SVUiTheme.color / SVUiTheme.alpha kullanılır. */
+const JS_CONTENT = {
+ 'admin.js': {
+ '#000000': [4, 'karartma ve palet: görüntünün kendi siyahı'],
+ '#2a1f2e': [2, 'arkaplanı olmayan sahnenin küçük resmi'],
+ '#161013': [2, 'arkaplanı olmayan sahnenin küçük resmi'],
+ '#08080f': [1, 'düz arkaplanlı sahnenin küçük resmi, renk yoksa'],
+ '#3aa6ff': [3, 'görselleştirici renginin varsayılanı'],
+ '#d24bff': [3, 'görselleştirici ikinci renginin varsayılanı'],
+ '#5b4be0': [1, 'dinamik tema paleti, kapak yokken'],
+ '#37e0c8': [1, 'dinamik tema paleti, kapak yokken'],
+ '#7be07b': [1, 'dinamik tema paleti, kapak yokken'],
+ },
+ 'clipdeck-panel.js': { '#4f8cff': [1, 'klip türü renginin yedeği'] },
+ 'record-panel.js': { '#000': [1, 'kaydın kendi karesi: görüntüden boş kalan kenar'] },
+ 'scene-panels.js': {
+ '#ff0055': [1, 'renk seçicinin yedeği'], '#ffffff': [3, 'metin ve Now Playing varsayılanı'],
+ '#ffd23f': [2, 'şarkı sözü vurgu varsayılanı'], '#c8c8d0': [1, 'Now Playing ikincil yazı varsayılanı'],
+ '#3aa6ff': [2, 'görselleştirici rengi varsayılanı'], '#ff2d3a': [1, 'görselleştirici rengi varsayılanı'],
+ '#0a0a12': [3, 'düz arkaplan varsayılanı'],
+ },
+ 'studio.js': { '#3aa6ff': [2, 'Studio renk parametresinin varsayılanı'], '#ffffff': [1, 'Studio renk parametresi'] },
+ 'template-panel.js': { '#333': [1, 'paleti olmayan şablonun şeridi'], '#777': [1, 'paleti olmayan şablonun şeridi'] },
+ 'timeline-panel.js': { '#4f8cff': [1, 'yeni klip parçasının rengi'], '#e11d2a': [1, 'yeni otomasyon parçasının rengi'] },
+};
+
+test('panel betiklerinde tema dışı renk yok (yalnız kullanıcı içeriği)', () => {
+ const dir = path.join(root, 'src', 'admin');
+ const got = {};
+ fs.readdirSync(dir).filter((f) => f.endsWith('.js')).forEach((f) => {
+ S.jsStringColors(fs.readFileSync(path.join(dir, f), 'utf8')).forEach((x) => {
+ got[f] = got[f] || {};
+ got[f][x.literal] = (got[f][x.literal] || 0) + 1;
+ });
+ });
+ const extra = [];
+ Object.keys(got).forEach((f) => Object.keys(got[f]).forEach((lit) => {
+ const allow = JS_CONTENT[f] && JS_CONTENT[f][lit];
+ if (!allow || got[f][lit] > allow[0]) extra.push(f + ': ' + lit + ' x' + got[f][lit]);
+ }));
+ assert.deepStrictEqual(extra, [], 'panelin rengi için SVUiTheme.color/alpha kullanın; kullanıcı içeriğiyse JS_CONTENT\'e gerekçesiyle ekleyin');
+ // Liste bayatlamasın: kalkan bir renk listeden de çıkmalı
+ const stale = [];
+ Object.keys(JS_CONTENT).forEach((f) => Object.keys(JS_CONTENT[f]).forEach((lit) => {
+ if (!got[f] || got[f][lit] !== JS_CONTENT[f][lit][0]) stale.push(f + ': ' + lit + ' bekleniyor x' + JS_CONTENT[f][lit][0] + ', bulundu x' + ((got[f] && got[f][lit]) || 0));
+ }));
+ assert.deepStrictEqual(stale, []);
+});
+
+test('tuvaller rengi temadan alıyor ve tema değişince yeniden çiziyor', () => {
+ ['src/admin/timeline-panel.js', 'src/admin/mapping-panel.js'].forEach((f) => {
+ const s = read(f);
+ assert.match(s, /SVUiTheme/, f);
+ assert.match(s, /SVUiTheme\.onChange\(/, f + ' yeniden çizim');
+ assert.doesNotMatch(s, /(fillStyle|strokeStyle)\s*=\s*[^;]*['"](#|rgba?\()/, f + ' tuvale sabit renk');
+ });
+});
+
+test('index.html: renk yalnız logo çiziminde; tema betikleri içinde', () => {
+ const html = read('src/admin/index.html');
+ const noLogo = html.replace(//, '');
+ const lits = (noLogo.match(/#[0-9a-fA-F]{3,8}\b(?![^<]*-->)|rgba?\(\s*\d/g) || [])
+ .filter((l) => !/^#\d{3,4}$/.test(l)); // yorumlardaki issue numaraları değil
+ assert.deepStrictEqual(lits, []);
+ const head = html.slice(0, html.indexOf(''));
+ const a = head.indexOf('src="../shared/ui-themes.js"');
+ const b = head.indexOf('src="theme-boot.js"');
+ assert.ok(a > 0 && b > a, 'ui-themes.js ve sonra theme-boot.js içinde');
+});
+
+// ---------------------------------------------------------------------------
+// Seçim ve uygulama
+// ---------------------------------------------------------------------------
+test('normalizeId ve resolve', () => {
+ assert.strictEqual(T.normalizeId('light'), 'light');
+ assert.strictEqual(T.normalizeId('system'), 'system');
+ ['', null, undefined, 'yok', '__proto__', 'constructor', 42].forEach((v) => assert.strictEqual(T.normalizeId(v), 'cayadev', String(v)));
+ assert.strictEqual(T.resolve('system', true).id, 'dark');
+ assert.strictEqual(T.resolve('system', false).id, 'light');
+ assert.strictEqual(T.resolve('system').id, 'dark', 'tercih bilinmiyorsa koyu');
+ assert.strictEqual(T.resolve('neon', false).id, 'neon', 'somut tema tercihe bakmaz');
+ assert.strictEqual(T.resolve('bilinmeyen', false).id, 'cayadev');
+});
+
+function fakeDom(search, prefersDarkInit) {
+ const props = new Map();
+ const attrs = {};
+ const mqListeners = [];
+ const mq = {
+ matches: prefersDarkInit,
+ addEventListener: (ev, fn) => mqListeners.push(fn),
+ };
+ const replaced = [];
+ const docEl = {
+ style: {
+ setProperty: (k, v) => props.set(k, v),
+ removeProperty: (k) => props.delete(k),
+ colorScheme: '',
+ },
+ setAttribute: (k, v) => { attrs[k] = v; },
+ };
+ const win = {
+ location: { search, href: 'file:///x/index.html' + search },
+ history: { state: null, replaceState: (s, t, u) => replaced.push(u) },
+ matchMedia: () => mq,
+ console,
+ };
+ win.window = win;
+ const ctx = vm.createContext({ window: win, document: { documentElement: docEl }, URL, URLSearchParams, console });
+ vm.runInContext(read('src/shared/ui-themes.js'), ctx);
+ vm.runInContext(read('src/admin/theme-boot.js'), ctx);
+ return {
+ win, props, attrs, docEl, replaced,
+ flipScheme: (dark) => { mq.matches = dark; mqListeners.forEach((fn) => fn()); },
+ };
+}
+
+test('theme-boot: adresteki tema gövdeden önce uygulanıyor', () => {
+ const d = fakeDom('?uiTheme=light', true);
+ assert.strictEqual(d.win.SVUiTheme.choice(), 'light');
+ assert.strictEqual(d.props.get('--bg'), T.byId('light').tokens['--bg']);
+ assert.strictEqual(d.docEl.style.colorScheme, 'light');
+ assert.strictEqual(d.attrs['data-ui-theme'], 'light');
+ assert.strictEqual(d.props.size, T.TOKEN_NAMES.length, 'bütün değişkenler yazıldı');
+});
+
+test('theme-boot: CaYaDev satır içi değerleri siler (admin.css :root geçerli)', () => {
+ const d = fakeDom('?uiTheme=neon', true);
+ assert.ok(d.props.size > 0);
+ d.win.SVUiTheme.set('cayadev');
+ assert.strictEqual(d.props.size, 0);
+ assert.strictEqual(d.docEl.style.colorScheme, 'dark');
+ assert.ok(d.replaced.some((u) => /uiTheme=cayadev/.test(u)), 'adres güncellendi (yeniden yükleme aynı temayla)');
+});
+
+test('theme-boot: bilinmeyen tema CaYaDev, sorgu yoksa CaYaDev', () => {
+ assert.strictEqual(fakeDom('?uiTheme=