Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,26 @@ updater sees v3.1.5 as the latest release and does not offer it to the beta as a
- **The selected scene's info line** was 3.9:1 in Light and 4.1–4.4:1
in Midnight and CaYaDev. On the selected row's accent tint it now uses the secondary text
colour, and the contrast rules cover body and secondary text on selected rows.
- **Screen readers and keyboards [777].**
- Chromium's accessibility tree showed 139 controls without a name: 54 sliders, 53 switches,
21 lists and some number and text boxes. A screen reader said only "slider". The setting's
label sat beside the control but was not tied to it.
- Each row's label text is now linked to its controls. The "reset to default" button inside the
label stays out of the name. The name follows the language, because the linked text is the
same node the translation updates.
- A few controls with their own layout got a name of their own: the A/B crossfader, the Smart
Silence Filter switch, the MCP port, and the timeline's BPM, beats-per-bar and snapping
controls.
- Pressing Tab through the panel reached 606 elements. On 68 of them focus could not be seen:
- sliders, which removed the outline and changed only on mouse hover;
- the export and update lists, where an inline border overrode the focus style;
- the lighting picker.

All of them now show focus.
- The self-test reads the accessibility tree in every category with folds open and walks
every category with real Tab presses. It fails on any control without a name or any
focusable element whose look does not change on focus. In the self-test's panel that is
872 named controls and 595 focusable elements.
- **The self-test scans a populated panel [777].**
- The self-test's settings had no saved scenes, no clip cells with a preview and no slot without
a source. So the untranslated-text scan and the contrast audit never drew those screens. The
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ layers and **40 GPU effects**, an **OBS overlay**, **Spout / Syphon**, **project
[![Downloads](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=downloads)](https://github.com/CaYatur/SoundVisualizer/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-e11d2a.svg)](LICENSE)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#download)
[![Tests](https://img.shields.io/badge/tests-3234%20passing-2ea043.svg)](#tests)
[![Tests](https://img.shields.io/badge/tests-3238%20passing-2ea043.svg)](#tests)
[![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/)

<a href="https://github.com/CaYatur/SoundVisualizer/releases/latest"><img src="https://img.shields.io/badge/Download-Windows-0a84ff?style=for-the-badge" alt="Download for Windows" /></a>
Expand Down Expand Up @@ -1746,7 +1746,7 @@ npm test
npm start -- --smoke
```

**3234 unit tests, all passing.** They are written to check answers, not to exercise lines:
**3238 unit tests, all passing.** They are written to check answers, not to exercise lines:

- **Formulas** are checked against values derived by hand from their definitions — Viviani's curve
staying on its sphere, the torus tube radius, Chladni's m↔n antisymmetry, every attractor staying
Expand Down
4 changes: 2 additions & 2 deletions README.tr.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ destesi**, ve **MCP üzerinden yapay zekâ kontrolü**.
[![İndirme](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=indirme)](https://github.com/CaYatur/SoundVisualizer/releases)
[![Lisans: MIT](https://img.shields.io/badge/Lisans-MIT-e11d2a.svg)](LICENSE)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#hızlı-başlangıç)
[![Test](https://img.shields.io/badge/test-3234%20geçiyor-2ea043.svg)](#testler)
[![Test](https://img.shields.io/badge/test-3238%20geçiyor-2ea043.svg)](#testler)
[![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/)

<a href="https://github.com/CaYatur/SoundVisualizer/releases/latest"><img src="https://img.shields.io/badge/%C4%B0ndir-Windows-0a84ff?style=for-the-badge" alt="Windows için indir" /></a>
Expand Down Expand Up @@ -1749,7 +1749,7 @@ npm test
npm start -- --smoke
```

**3234 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar:
**3238 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar:

- **Formüller**, tanımlarından elle türetilmiş değerlerle sınanıyor — Viviani eğrisinin küre üzerinde
kalması, simidin boru yarıçapı, Chladni'nin m↔n antisimetrisi, her çekicinin sınırlı kalması ve görüş
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ npm test
npm start -- --smoke
```

- **3234 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0;
- **3238 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0;
105 came with v3.1.1; 163 came with v3.1.2; 157 came with v3.1.3 — 1128 at
that tag — 469 more with v3.1.4, most of them from the MilkDrop work — 1597 at
that tag — 1402 more with v3.1.5 — 2999 at that tag — and 27
Expand Down
8 changes: 8 additions & 0 deletions src/admin/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,8 @@ body {
position: relative; font-size: 12px; color: var(--text);
display: flex; align-items: center; gap: 6px; padding-right: 20px;
}
/* Erişilebilir adın metni (#777): etiketin iç düzeni değişmesin */
.ctrl label.lbl > .lbl-t { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.ctrl .val {
font-size: 11px; color: var(--muted); min-width: 46px; text-align: right;
font-variant-numeric: tabular-nums;
Expand Down Expand Up @@ -543,6 +545,10 @@ input[type=range]::-webkit-slider-thumb {
transition: box-shadow .15s;
}
input[type=range]::-webkit-slider-thumb:hover { box-shadow: 0 0 0 5px rgb(var(--accent-rgb) / .24); }
/* Klavye odağı (#777): `outline: none` vardı ve başparmak yalnız fareyle
değişiyordu; Tab ile gelen kaydırıcı hiç belli olmuyordu. */
input[type=range]:focus-visible { outline: 2px solid var(--accent); outline-offset: 5px; }
input[type=range]:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 5px rgb(var(--accent-rgb) / .35); }

/* toggle */
.switch { position: relative; width: 40px; height: 22px; flex-shrink: 0; }
Expand Down Expand Up @@ -596,6 +602,7 @@ select {
}
select:focus { border-color: var(--accent); }
.ctrl .row + select { margin-top: 7px; }
select.sel-ctrl { font-size: 13px; margin-top: 6px; }
/* Etiketle AYNI satırdaki liste (#622). Genel `select` kuralı genişliği %100
ve üst boşluğu 7 px veriyordu: liste satırı ezip etiketi iki satıra
kırıyor ("Resim / Kaynağı") ve etiketten 7 px aşağıda duruyordu. Artık
Expand Down Expand Up @@ -1050,6 +1057,7 @@ kbd { background: var(--card2); border: 1px solid var(--line); border-radius: 5p
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-wrap:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); }
.lighting-select-button {
width: 100%; min-height: 40px; display: flex; align-items: center; justify-content: space-between;
gap: 12px; padding: 9px 12px; border: 1px solid var(--line); border-radius: var(--radius-sm);
Expand Down
63 changes: 61 additions & 2 deletions src/admin/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,64 @@
hidden: 1, required: 1, open: 1, autofocus: 1, novalidate: 1,
};

/* ERİŞİLEBİLİR ADLAR (#777). Ayar satırı `.ctrl` içinde bir `label.lbl`
ve bir denetim, ama etiket denetime bağlı değildi: Chromium'un
erişilebilirlik ağacında 139 denetimin (54 kaydırıcı, 53 anahtar, 21
liste…) adı boştu ve ekran okuyucu yalnız "kaydırıcı" diyordu. Etiketin
YALNIZ METNİ bir span'e alınıyor (içindeki "varsayılana döndür" düğmesi
adın parçası olmasın) ve aynı satırdaki adsız denetimler ona
`aria-labelledby` ile bağlanıyor. Metin düğümü taşınıyor, değişmiyor:
çeviri onu yine buluyor ve ad dil değişince kendiliğinden güncel.
Etiketi olmayan satırda grubun başlığı (`.group-label`) kullanılıyor.
Paneller sonradan da çizildiği için bir gözlemci çalıştırıyor. */
let labelSeq = 0;
function hasName(c) {
if (c.getAttribute('aria-label') || c.getAttribute('aria-labelledby') || c.getAttribute('title')) return true;
const ls = c.labels;
if (ls) for (let i = 0; i < ls.length; i++) if (ls[i].textContent.trim()) return true;
return false;
}
function labelTextOf(lbl) {
let t = lbl.querySelector(':scope > .lbl-t');
if (t) return t;
const kids = Array.from(lbl.childNodes).filter((n) => !(n.nodeType === 1 && /^(BUTTON|INPUT|SELECT|TEXTAREA)$/.test(n.tagName)));
if (!kids.some((n) => (n.textContent || '').trim())) return null;
t = document.createElement('span');
t.className = 'lbl-t';
lbl.insertBefore(t, kids[0]);
kids.forEach((n) => t.appendChild(n));
return t;
}
function linkLabels(root) {
root.querySelectorAll('.ctrl').forEach((c) => {
const ctls = Array.from(c.querySelectorAll('input:not([type=hidden]), select, textarea'))
.filter((x) => x.closest('.ctrl') === c && !hasName(x));
if (!ctls.length) return;
const lbl = c.querySelector(':scope > label.lbl, :scope > .row > label.lbl');
let t = lbl ? labelTextOf(lbl) : null;
if (!t) {
const prev = c.previousElementSibling;
if (prev && prev.classList.contains('group-label') && prev.textContent.trim()) t = prev;
}
if (!t) return;
if (!t.id) t.id = 'svlbl-' + (++labelSeq);
ctls.forEach((x) => x.setAttribute('aria-labelledby', t.id));
});
}
/* Gözlemci geri çağrısı zaten toplu (mikro görev). requestAnimationFrame
KULLANILMIYOR: arka plandaki ya da küçültülmüş pencerede hiç çalışmıyor
ve adlar pencere görünene kadar boş kalıyordu. Bağlayıcının kendi
değişiklikleri gözlemciyi bir kez daha tetikler; ikinci tur bir şey
bulamaz ve durur. */
if (typeof MutationObserver === 'function' && typeof document !== 'undefined' && document.body) {
let busy = false;
new MutationObserver(() => {
if (busy) return;
busy = true;
try { linkLabels(document); } finally { busy = false; }
}).observe(document.body, { childList: true, subtree: true });
}

function el(tag, props, kids) {
const e = document.createElement(tag);
if (props) {
Expand Down Expand Up @@ -889,8 +947,9 @@
opt.selected = true;
sel.appendChild(opt);
}
sel.style.width = '100%';
sel.style.cssText += 'background:var(--card2);color:var(--text);border:1px solid var(--line);border-radius:9px;padding:8px 10px;font-size:13px;margin-top:6px;outline:none;';
/* Görünüş sınıfta: satır içi kenarlık `select:focus`u eziyor ve
klavye odağı görünmüyordu (#777). */
sel.classList.add('sel-ctrl');
return el('div', { class: 'ctrl' }, [el('label', { class: 'lbl', text: def.label }), sel]);
}

Expand Down
2 changes: 2 additions & 0 deletions src/admin/analysis-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
const guardSwitch = el('input', {
type: 'checkbox',
'data-path': 'audio.humGuard',
// Başlığı anahtarın yanında ayrı bir öğe (#777)
'aria-label': 'Akıllı Sessizlik Filtresi',
onchange: (e) => {
const val = e.target.checked;
if (P().set) P().set('audio.humGuard', val);
Expand Down
2 changes: 2 additions & 0 deletions src/admin/group-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
const slider = el('input', {
type: 'range', min: 0, max: 1, step: 0.005, value: x.value == null ? 0 : x.value,
class: 'xf-slider',
// Etiketi yok, iki ucunda A ve B yazıyor (#777)
'aria-label': 'A/B Çapraz Geçiş',
oninput: (e) => {
x.value = parseFloat(e.target.value);
val.textContent = label(x.value);
Expand Down
2 changes: 1 addition & 1 deletion src/admin/mcp-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@
const busy = status.requestedPort || status.configuredPort || model.port;
nodes.push(el('div', { class: 'studio-note', text: tx('mcp.port.busy').split('{port}').join(String(busy)) }));
}
const portInput = el('input', { type: 'number', min: '1', max: '65535', value: String(model.port || 38471) });
const portInput = el('input', { type: 'number', min: '1', max: '65535', value: String(model.port || 38471), 'aria-label': tx('mcp.port.label') });
nodes.push(el('div', { class: 'mcp-port' }, [
el('label', { class: 'lbl', text: tx('mcp.port.label') }),
portInput,
Expand Down
19 changes: 12 additions & 7 deletions src/admin/timeline-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -1799,18 +1799,23 @@
tempo değişimleri sessizce siliniyordu. Değişimler cetvelde (♩). */
group([
el('span', { class: 'tl-lbl', text: 'BPM', title: 'Oynatma kafasındaki tempo. Tempo değişimleri cetvelde (♩)' }),
bpmIn,
// Araç çubuğunda etiketler ayrı öğe: denetimlerin kendi adı (#777)
(bpmIn.setAttribute('aria-label', 'Oynatma kafasındaki tempo (BPM)'), bpmIn),
el('span', { class: 'tl-lbl', text: '/' }),
bpbIn,
(bpbIn.setAttribute('aria-label', 'Ölçüdeki vuruş sayısı'), bpbIn),
btn(['plus', '♩'], 'Oynatma kafasına tempo değişimi', () => addTempoAtHead()),
]),
group([
el('span', { class: 'tl-lbl', icon: 'magnet', title: 'Yakalama. Sürüklerken Alt tuşu yakalamayı geçici olarak kapatır' }),
select(
[['off', 'Kapalı'], ['bar', 'Ölçü'], ['beat', 'Vuruş'], ['half', 'Yarım Vuruş'], ['quarter', 'Çeyrek Vuruş'], ['frame', 'Kare']],
cfg.snap,
(v) => { cfg.snap = v; p.push(true); }
),
(() => {
const s = select(
[['off', 'Kapalı'], ['bar', 'Ölçü'], ['beat', 'Vuruş'], ['half', 'Yarım Vuruş'], ['quarter', 'Çeyrek Vuruş'], ['frame', 'Kare']],
cfg.snap,
(v) => { cfg.snap = v; p.push(true); }
);
s.setAttribute('aria-label', 'Yakalama');
return s;
})(),
]),
group([loopBtn, followBtn]),
group([
Expand Down
80 changes: 80 additions & 0 deletions src/main/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7274,6 +7274,86 @@ async function runSmoke() {
} catch (e) {
errors.push('ui themes: ' + (e && e.message ? e.message : e));
}
/* --- Erişilebilirlik (#777): adsız denetim ve görünmeyen klavye odağı ---
Ad Chromium'un kendi erişilebilirlik ağacından okunur (ekran okuyucunun
duyduğu ad). Odak gerçek Tab tuşlarıyla dolaşılır; odaklanan öğenin
görünüşü odaksız anlık görüntüsüyle aynıysa odak görünmüyordur.
İlk ölçümde 139 adsız denetim ve 68 görünmeyen odak vardı. */
try {
const dbg = awc4.debugger;
let attached = false;
try { dbg.attach('1.3'); attached = true; } catch (e) { console.log('[SMOKE] erişilebilirlik: hata ayıklayıcı bağlanamadı, atlandı'); }
if (attached) {
await dbg.sendCommand('Accessibility.enable');
await dbg.sendCommand('DOM.enable');
const ROLES = new Set(['button', 'link', 'checkbox', 'radio', 'switch', 'tab', 'combobox', 'textbox', 'slider',
'spinbutton', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'listbox', 'searchbox', 'treeitem']);
const cats = await awc4.executeJavaScript("Array.from(document.querySelectorAll('[data-cat]')).map(function (b) { return b.dataset.cat; })");
const unnamed = new Map();
const hidden = new Map();
let named = 0;
let tabbed = 0;
const tab = async () => {
await dbg.sendCommand('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Tab', code: 'Tab', windowsVirtualKeyCode: 9 });
await dbg.sendCommand('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Tab', code: 'Tab', windowsVirtualKeyCode: 9 });
};
for (const c of cats) {
await awc4.executeJavaScript(`(async function () {
var b = document.querySelector('[data-cat="${c}"]'); if (b) b.click();
await new Promise(function (r) { setTimeout(r, 400); });
var h = document.querySelectorAll('#sections .fold-head:not(.open)');
for (var i = 0; i < h.length; i++) h[i].click();
await new Promise(function (r) { setTimeout(r, 300); });
var P = ['outline-style','outline-width','outline-color','box-shadow','border-top-color','border-bottom-color','background-color','color'];
window.__a11ySnap = function (e) { var s = getComputedStyle(e); return P.map(function (p) { return s.getPropertyValue(p); }).join('|'); };
window.__a11yBase = new Map();
document.querySelectorAll('#sections button, #sections a[href], #sections input, #sections select, #sections textarea, #sections [tabindex]').forEach(function (e) { window.__a11yBase.set(e, window.__a11ySnap(e)); });
if (document.activeElement) document.activeElement.blur();
return 1;
})()`);
const tree = (await dbg.sendCommand('Accessibility.getFullAXTree')).nodes || [];
for (const n of tree) {
if (n.ignored || !n.role || !ROLES.has(n.role.value)) continue;
if (String((n.name && n.name.value) || '').trim()) { named++; continue; }
let d = n.role.value;
try {
const nd = (await dbg.sendCommand('DOM.describeNode', { backendNodeId: n.backendDOMNodeId })).node;
const at = nd.attributes || [];
const cls = at[at.indexOf('class') + 1];
d += ' ' + nd.nodeName.toLowerCase() + (at.indexOf('class') >= 0 && cls ? '.' + cls.split(/\s+/).slice(0, 2).join('.') : '');
} catch (e) { /* düğüm gitti */ }
unnamed.set(d, (unnamed.get(d) || 0) + 1);
}
const seen = new Set();
for (let k = 0; k < 400; k++) {
await tab();
const r = await awc4.executeJavaScript(`(function () {
var a = document.activeElement; if (!a || a === document.body || !a.closest('#sections')) return null;
if (a.getAnimations) a.getAnimations().forEach(function (x) { try { x.finish(); } catch (e) { /* sonsuz */ } });
var base = window.__a11yBase.get(a);
if (!a.dataset.a11yk) a.dataset.a11yk = String(Math.random()).slice(2);
var d = a.tagName.toLowerCase() + (typeof a.className === 'string' && a.className ? '.' + a.className.trim().split(/\\s+/).slice(0, 2).join('.') : '');
return { k: a.dataset.a11yk, d: d, same: base !== undefined && base === window.__a11ySnap(a) };
})()`);
if (!r) { if (seen.size > 5) break; continue; }
if (seen.has(r.k)) break;
seen.add(r.k);
tabbed++;
if (r.same) hidden.set(r.d, (hidden.get(r.d) || 0) + 1);
}
}
try { dbg.detach(); } catch (e) { /* zaten ayrıldı */ }
console.log('[SMOKE] erişilebilirlik: ' + named + ' adlı denetim, ' + tabbed + ' odaklanan öğe');
unnamed.forEach((n, d) => console.log('[SMOKE] ~ adsız ' + n + ' × ' + d));
hidden.forEach((n, d) => console.log('[SMOKE] ~ odak görünmüyor ' + n + ' × ' + d));
if (unnamed.size) errors.push('a11y: ' + [...unnamed.values()].reduce((a, b) => a + b, 0) + ' control(s) without an accessible name');
if (hidden.size) errors.push('a11y: ' + [...hidden.values()].reduce((a, b) => a + b, 0) + ' focusable element(s) without a visible focus change');
if (named < 500) errors.push('a11y: only ' + named + ' named controls seen');
if (tabbed < 300) errors.push('a11y: only ' + tabbed + ' elements reached with Tab');
}
} catch (e) {
errors.push('a11y: ' + (e && e.message ? e.message : e));
}
// Dolu panel eski hâline (sahneler, yuvalar, seçili sahne)
try {
await awc4.executeJavaScript(`(function(){
Expand Down
Loading
Loading