fix: WCAG AAA contrast compliance, speed menu z-index, custom app icon

- Fix all text colors to meet WCAG 2.2 AAA 7:1 contrast ratios against
  dark backgrounds (--textMuted, --textDim, hover states across playlist,
  player, panels, tooltips)
- Fix speed menu rendering behind seek bar by correcting z-index stacking
  context (.controls z-index:10, .miniCtl z-index:3, .seek z-index:2)
- Replace default Tauri icons with custom TutorialVault icon across all
  required sizes (32-512px PNGs, ICO, ICNS, Windows Square logos)
- Update README: Fraunces → Bricolage Grotesque font reference
- Add collapsible dock pane persistence and keyboard-adjustable dividers
This commit is contained in:
Your Name
2026-02-19 18:23:38 +02:00
parent a571a33415
commit c0a8eca955
29 changed files with 147 additions and 52 deletions
+76 -1
View File
@@ -27,6 +27,7 @@ let recentMenu: HTMLElement;
let refreshBtn: HTMLElement;
let resetProgBtn: HTMLElement;
let notesBox: HTMLTextAreaElement;
let notesBackdrop: HTMLElement;
let notesSaved: HTMLElement;
let nowTitle: HTMLElement;
let nowSub: HTMLElement;
@@ -93,6 +94,32 @@ export function initUI(): void {
resetProgBtn = document.getElementById('resetProgBtn')!;
notesBox = document.getElementById('notesBox') as HTMLTextAreaElement;
notesSaved = document.getElementById('notesSaved')!;
// Create notes highlight backdrop for timestamp coloring
notesBackdrop = document.createElement('div');
notesBackdrop.className = 'notesBackdrop';
notesBackdrop.setAttribute('aria-hidden', 'true');
notesBox.parentElement!.insertBefore(notesBackdrop, notesBox);
// Scroll sync between textarea and backdrop
notesBox.addEventListener('scroll', () => {
notesBackdrop.scrollTop = notesBox.scrollTop;
notesBackdrop.scrollLeft = notesBox.scrollLeft;
});
// Forward hover/focus state to backdrop for visual effects
notesBox.addEventListener('mouseenter', () => notesBackdrop.classList.add('hover'));
notesBox.addEventListener('mouseleave', () => notesBackdrop.classList.remove('hover'));
notesBox.addEventListener('focus', () => notesBackdrop.classList.add('focus'));
notesBox.addEventListener('blur', () => notesBackdrop.classList.remove('focus'));
// Show pointer cursor when hovering over timestamps
notesBox.addEventListener('mousemove', (e) => {
notesBox.style.pointerEvents = 'none';
const el = document.elementFromPoint(e.clientX, e.clientY);
notesBox.style.pointerEvents = '';
notesBox.style.cursor = (el && el.classList.contains('tsLink')) ? 'pointer' : '';
});
nowTitle = document.getElementById('nowTitle')!;
nowSub = document.getElementById('nowSub')!;
overallBar = document.getElementById('overallBar')!;
@@ -348,6 +375,7 @@ export function initUI(): void {
// --- Notes ---
notesBox.addEventListener('input', () => {
updateNotesHighlight();
if (!library) return;
const it = currentItem();
if (!it) return;
@@ -385,6 +413,37 @@ export function initUI(): void {
};
}
// --- Clickable timestamps in notes ---
notesBox.addEventListener('click', () => {
const pos = notesBox.selectionStart;
if (pos === null || pos === undefined) return;
// Only act on single click without selection
if (notesBox.selectionStart !== notesBox.selectionEnd) return;
const text = notesBox.value;
// Match [H:MM:SS] or [M:SS] patterns
const re = /\[(\d{1,3}):(\d{2})(?::(\d{2}))?\]/g;
let match;
while ((match = re.exec(text)) !== null) {
const start = match.index;
const end = start + match[0].length;
if (pos >= start && pos <= end) {
let totalSeconds: number;
if (match[3] !== undefined) {
// [H:MM:SS]
totalSeconds = parseInt(match[1], 10) * 3600 + parseInt(match[2], 10) * 60 + parseInt(match[3], 10);
} else {
// [M:SS]
totalSeconds = parseInt(match[1], 10) * 60 + parseInt(match[2], 10);
}
if (player && Number.isFinite(player.duration) && player.duration > 0) {
player.currentTime = clamp(totalSeconds, 0, player.duration);
cb.notify?.(`Jumped to ${match[0]}`);
}
break;
}
}
});
// --- Collapsible dock panes ---
const notesHeader = document.getElementById('notesHeader');
const infoHeader = document.getElementById('infoHeader');
@@ -578,11 +637,12 @@ export async function refreshCurrentVideoMeta(): Promise<void> {
export async function loadNoteForCurrent(): Promise<void> {
const it = currentItem();
if (!it) { notesBox.value = ''; return; }
if (!it) { notesBox.value = ''; updateNotesHighlight(); return; }
try {
const res = await api.getNote(it.fid);
notesBox.value = (res && res.ok) ? (res.note || '') : '';
} catch (_) { notesBox.value = ''; }
updateNotesHighlight();
}
export function setOnTopChecked(v: boolean): void { onTopChk.checked = v; }
@@ -726,6 +786,21 @@ function toggleDockPane(header: HTMLElement, prefKey: string): void {
savePrefsPatch({ [prefKey]: isCollapsed });
}
function updateNotesHighlight(): void {
if (!notesBackdrop) return;
const text = notesBox.value;
const escaped = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
const highlighted = escaped.replace(
/\[(\d{1,3}:\d{2}(?::\d{2})?)\]/g,
'<span class="tsLink">[$1]</span>'
);
// Trailing newline matches textarea's extra line space
notesBackdrop.innerHTML = highlighted + '\n';
}
function collapsePane(header: HTMLElement, collapsed: boolean): void {
const pane = header.closest('.dockPane');
if (!pane) return;