From de253563bad994be5e59a2bebea58946a2bfd9ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87A=C4=9EAN=20TURGUT?= Date: Sat, 17 Oct 2026 11:31:32 +0300 Subject: [PATCH 1/3] Interface themes: CaYaDev, Dark, Light, Device default and four more (#770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/shared/ui-themes.js: one table of 64 colour tokens per theme (CaYaDev, Dark, Light, High Contrast, Midnight, Sepia, Neon) plus Device default, which resolves from prefers-color-scheme and follows it live. - admin.css/studio.css: about 320 colour literals became tokens; channel tokens are used as rgb(var(--x-rgb) / a) so tints flip on light themes; :root is the CaYaDev table. - Timeline and projection-mapping canvases read tokens and redraw on a theme change. - theme-boot.js applies the theme in ; the main process opens the panel in the theme's background and passes the theme in the address. README shots stay CaYaDev; nativeTheme.themeSource is not touched. - Settings → Application → Interface Theme picker (radio group, arrow keys); the setting lives in ui.theme, travels with backups and survives Reset to Defaults; MCP group output. - CaYaDev: --muted2, code comments and the solid danger button raised to 4.5:1; the button gradient end is its own token (--accent-grad) so white text reads mid-gradient. - Fixed two labels below 3:1 in every theme (crossfader empty side, Studio built-in tag); decorative separators are aria-hidden. - Tests: token completeness, :root parity, no colour literals outside the table (CSS and panel scripts), defined variables, 128 contrast rules per theme, every CSS text/background pair per theme, head script in a fake DOM. Self-test measures the drawn contrast of every visible text in every theme. Co-authored-by: caganturgut <271973834+caganturgut@users.noreply.github.com> --- src/admin/admin.css | 371 +++++++++++++++---------- src/admin/admin.js | 112 +++++++- src/admin/index.html | 3 + src/admin/mapping-panel.js | 31 ++- src/admin/openrgb-panel.js | 4 +- src/admin/scene-panels.js | 4 +- src/admin/stream.js | 4 +- src/admin/studio.css | 264 +++++++++--------- src/admin/texture-panel.js | 6 +- src/admin/theme-boot.js | 86 ++++++ src/admin/timeline-panel.js | 64 +++-- src/main/main.js | 102 ++++++- src/shared/defaults.js | 8 + src/shared/i18n.js | 9 + src/shared/mcp.js | 2 + src/shared/ui-contrast-audit.js | 175 ++++++++++++ src/shared/ui-theme-contrast.js | 118 ++++++++ src/shared/ui-themes.js | 337 +++++++++++++++++++++++ tests/helpers/theme-scan.js | 158 +++++++++++ tests/logo-lib-ui.test.js | 5 +- tests/ui-themes.test.js | 472 ++++++++++++++++++++++++++++++++ 21 files changed, 2009 insertions(+), 326 deletions(-) create mode 100644 src/admin/theme-boot.js create mode 100644 src/shared/ui-contrast-audit.js create mode 100644 src/shared/ui-theme-contrast.js create mode 100644 src/shared/ui-themes.js create mode 100644 tests/helpers/theme-scan.js create mode 100644 tests/ui-themes.test.js diff --git a/src/admin/admin.css b/src/admin/admin.css index f2170eab..da226968 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,14 +1336,14 @@ 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; @@ -1290,23 +1351,23 @@ input[type=color].tl-swatch.big { width: 34px; height: 24px; } } .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); } +.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 +1391,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 +1446,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(--cue-armed); border-left: 2px solid var(--cue-armed); 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 +1456,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 +1493,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 +1503,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 +1580,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 +1630,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 dc499fdf..761e3923 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. */ @@ -7109,6 +7141,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 1fa51674..ecf0a19e 100644 --- a/src/shared/i18n.js +++ b/src/shared/i18n.js @@ -2968,6 +2968,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..7def2856 --- /dev/null +++ b/src/shared/ui-theme-contrast.js @@ -0,0 +1,118 @@ +/* + * 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', + '--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 etiketi + ['--card', '--card2'].forEach((base) => { + out.push({ fg: '--tempo', on: { tint: '--fg-rgb', alpha: 0.04, base }, min: 'text', why: 'tempo etiketi / ' + base }); + }); + // 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.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..0bf623c4 --- /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': '#b07800', '--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': '#a06800', '--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..42756684 --- /dev/null +++ b/tests/ui-themes.test.js @@ -0,0 +1,472 @@ +/* + * 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(/ 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=