Compare commits

..

28 Commits

Author SHA1 Message Date
lashman 24e57a788f Update README.md 2026-03-09 16:30:41 +02:00
Your Name 58847c018f docs: strip emojis from README list items, keep only in headers 2026-02-19 18:29:29 +02:00
Your Name c0a8eca955 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
2026-02-19 18:23:38 +02:00
Your Name a571a33415 docs: update README with all recent features and changes
- Add new playback features: mute toggle, PiP, fullscreen, seek feedback, error overlay
- Expand keyboard shortcuts table with F, M, [, ], ? bindings
- Add playlist search/filter, scroll-to-current, mini progress bars
- Add timestamp insertion for notes
- Add two-click reset confirmation, collapsible dock panes, custom titlebar
- Add full accessibility section (WCAG 2.2 AAA)
- Add architecture module breakdown and Cold Open design theme details
2026-02-19 17:10:33 +02:00
Your Name 1b2ebd807c chore: update fonts, Tauri config, and reduced-motion support
- Switch font deps from Sora/Manrope/IBM Plex Mono to Fraunces/Inter/Space Mono (Cold Open theme)
- Add Tauri window permissions for custom titlebar controls
- Disable native decorations and drag-drop in Tauri config
- Add prefers-reduced-motion media query for WCAG compliance
2026-02-19 17:03:39 +02:00
Your Name 715c3c713a feat: add 15 UI enhancements
1. Mute toggle (M key + volume icon click)
2. Fullscreen shortcut (F key)
3. Seek feedback overlay (±5s flash with accumulation)
4. Playlist search/filter with clear button
5. Scroll-to-current button (IntersectionObserver)
6. Picture-in-Picture button
7. Timestamp insertion in notes
8. Keyboard shortcut help panel (? key)
9. Playback speed shortcuts ([ and ] keys)
10. Reset progress two-click confirmation
11. Video load error state overlay
12. Double-click video to fullscreen
13. Playlist stats in header (count + done)
14. Mini progress bars per playlist item
15. Collapsible dock panes with chevron icons

All enhancements are WCAG 2.2 AAA compliant with proper
aria-labels, aria-live regions, focus-visible states,
keyboard accessibility, and 44x44 touch targets.
2026-02-19 17:01:01 +02:00
Your Name 98011cf604 style: remove controls container and bump surface contrast
Strip background/border-radius from .controlsStrip so playback
controls float freely. Roughly double all --surface opacity values
for better element visibility against the dark slate background.
2026-02-19 16:54:12 +02:00
Your Name c2533e8a76 docs: add design for 15 UI enhancements
Mute toggle, fullscreen shortcut, seek feedback overlay, playlist
search, scroll-to-current, PiP, timestamp insertion, keyboard help,
speed shortcuts, reset confirmation, error state, double-click
fullscreen, playlist stats, per-item progress bars, collapsible docks.
2026-02-19 16:51:54 +02:00
Your Name a6f1aef489 fix: reset heading margins and button chrome from semantic HTML changes
The WCAG pass changed <div> to <h3> for dock headers and <span> to
<button> for the zoom reset label. Browser defaults for those elements
(heading margins, button border/background) were never overridden,
causing bloated headers and a Windows-XP-style zoom button.
2026-02-19 16:40:39 +02:00
Your Name cd362a29b1 a11y: bring UI to WCAG 2.2 AAA compliance
Semantic HTML: lang attr, landmarks (header/main/region/complementary),
heading hierarchy (h1-h3), dl/dt/dd for info panel.

ARIA: labels on all icon buttons, aria-hidden on decorative icons,
progressbar role with dynamic aria-valuenow, aria-haspopup/expanded
on all menu triggers, role=listbox/option on playlist, aria-selected,
computed aria-labels on playlist rows.

Contrast: raised --textMuted/--textDim/--icon to AAA 7:1 ratios.

Focus: global :focus-visible outline, slider thumb glow, menu item
highlight, switch focus-within, row focus styles.

Target sizes: 44x44 hit areas on zoom/window/remove buttons via
::before pseudo-elements.

Keyboard: playlist arrow nav + Enter/Space activate + Alt+Arrow
reorder with live region announcements + move buttons. Speed menu,
subtitles menu, and recent menu all keyboard-navigable with
Arrow/Enter/Space/Escape. Dividers resizable via Arrow keys.

Dynamic document.title updates on video/folder load.
2026-02-19 16:35:19 +02:00
Your Name 600188eb1a docs: add WCAG 2.2 AAA implementation plan 2026-02-19 16:00:19 +02:00
Your Name 17e4ffd28f docs: add WCAG 2.2 AAA remediation design 2026-02-19 15:54:04 +02:00
Your Name f3aa5f7937 Move and expand philosophy section 2026-02-19 12:58:55 +02:00
Your Name 1579e146b5 Replace fancy dashes and quotes with plain ASCII 2026-02-19 12:56:30 +02:00
Your Name 17b57dbab0 Fancy up README header with badges 2026-02-19 12:54:43 +02:00
Your Name 8860188055 Add emoji to README 2026-02-19 12:53:50 +02:00
Your Name 26ff5cd95b Add README 2026-02-19 12:52:21 +02:00
Your Name 9c8d7d94cd TutorialVault: complete Tauri v2 port with runtime fixes
Rename from TutorialDock to TutorialVault. Remove legacy Python app
and scripts. Fix video playback, subtitles, metadata display, window
state persistence, and auto-download of ffmpeg/ffprobe on first run.
Bundle fonts via npm instead of runtime download.
2026-02-19 12:44:57 +02:00
Your Name a459efae45 feat: implement all frontend TypeScript modules
Create player.ts (video controls, seek, volume, speed, overlay),
playlist.ts (list rendering, tree SVG, drag-and-drop reorder, scrollbar),
subtitles.ts (subtitle menu, track management, sidecar/embedded),
ui.ts (zoom, splits, info panel, notes, toasts, recent menu),
tooltips.ts (zoom-aware tooltip system with delays),
store.ts (shared state and utility functions), and
main.ts (boot sequence, tick loop, keyboard shortcuts).

All modules compile with strict TypeScript. Vite build produces
34KB JS + 41KB CSS. 115 Rust tests pass.
2026-02-19 11:44:48 +02:00
Your Name 4e454084a8 feat: implement video_protocol.rs, commands.rs, wire up main.rs, and index.html
- video_protocol.rs: tutdock:// custom protocol with HTTP Range support
  for video streaming, subtitle/font serving with path traversal protection
- commands.rs: all 26 Tauri command handlers as thin wrappers
- main.rs: full Tauri bootstrap with state management, window restore,
  async font caching, and ffmpeg discovery
- index.html: complete HTML markup extracted from Python app
- lib.rs: updated with all module declarations and AppPaths struct
2026-02-19 11:23:37 +02:00
Your Name 9c8474d24f feat: implement library.rs, types.ts, api.ts, and extract CSS
- library.rs: full video library management (1948 lines, 10 tests)
  folder scanning, progress tracking, playlists, subtitle integration,
  background duration scanning
- types.ts: all TypeScript interfaces for Tauri command responses
- api.ts: typed wrappers for all 26 Tauri invoke commands
- 6 CSS files extracted from Python HTML into src/styles/
2026-02-19 02:08:23 +02:00
Your Name 4e91fe679f feat: implement ffmpeg.rs, subtitles.rs, and fonts.rs
- ffmpeg.rs: discovery, duration extraction, metadata probing, download
- subtitles.rs: SRT-to-VTT conversion, sidecar discovery, storage, extraction
- fonts.rs: Google Fonts and Font Awesome local caching
2026-02-19 01:59:21 +02:00
Your Name 3280d60f71 feat: implement recents.rs - recent folders management 2026-02-19 01:50:28 +02:00
Your Name b95094c50f feat: implement prefs.rs - preferences with save/load/update 2026-02-19 01:50:20 +02:00
Your Name 6ecbeb9a9b feat: implement utils.rs and state.rs
utils.rs: natural sort, file fingerprinting, library ID computation,
pretty title formatting, path helpers, clamp, truthy (38 tests)

state.rs: atomic JSON persistence with backup rotation and
fallback loading (8 tests)
2026-02-19 01:47:37 +02:00
Your Name 40d7ce4291 fix: remove protocol-asset feature, add app icons for build 2026-02-19 01:29:50 +02:00
Your Name 24746bb33d feat: scaffold Tauri v2 + Vite + TypeScript project 2026-02-19 01:23:27 +02:00
Your Name 47d4409bcc initial: existing TutorialDock Python app + conversion design and plan 2026-02-19 01:16:17 +02:00
20 changed files with 1653 additions and 61 deletions
+13 -15
View File
@@ -69,7 +69,7 @@ This is that tool.
**Full-featured player.** Play, pause, seek, adjust volume, change playback speed, and go fullscreen. All the controls you'd expect, with a clean minimal interface that stays out of your way.
**Playback speed.** Presets from 0.50x to 2.00x, with a visual speedometer that changes color based on your current rate. Speed is saved per folder, so your lecture courses stay at 1.5x while your coding tutorials stay at 1x. Cycle through speeds with keyboard shortcuts.
**Playback speed.** Presets from 0.50x to 2.00x, with a visual speedometer that changes color based on your current rate. Speed is saved per folder, so your lecture courses stay at 1.5x while your drawing tutorials stay at 1x. Cycle through speeds with keyboard shortcuts.
**Volume control.** A slider with a live percentage tooltip. Click the volume icon to mute/unmute instantly. Volume is saved per folder as well.
@@ -112,7 +112,7 @@ Press **?** at any time to open a help dialog listing all available shortcuts.
**Resume from where you left off.** Clicking any video in the playlist, or navigating with previous/next, resumes from your last position. Finished videos start from the beginning.
**Progress reset.** If you want to go through a course again from scratch, a reset button clears all watch progress while preserving your notes, subtitles, volume, and speed settings. Uses a two-click confirmation to prevent accidental resets - the first click arms the button (it turns red with a warning icon), and the second click within 3 seconds actually resets. If you change your mind, just wait or click elsewhere.
**Progress reset.** If you want to go through a course again from scratch, a reset button clears all watch progress while preserving your notes, subtitles, volume, and speed settings. Uses a two-click confirmation to prevent accidental resets -- the first click arms the button (it turns red with a warning icon), and the second click within 3 seconds actually resets. If you change your mind, just wait or click elsewhere.
**Mini progress bars.** Each item in the playlist shows a thin progress bar at the bottom of its row, visualizing how far you've watched. Completed videos get a green bar; in-progress videos show the accent color proportional to watched time.
@@ -204,7 +204,7 @@ All of this requires ffprobe, which TutorialVault will automatically download if
TutorialVault targets WCAG 2.2 AAA compliance throughout the interface.
**Semantic HTML.** The interface uses proper semantic elements - `<nav>`, `<main>`, `<header>`, `<section>`, `<h2>`, `<h3>` - so screen readers can navigate the structure meaningfully.
**Semantic HTML.** The interface uses proper semantic elements -- `<nav>`, `<main>`, `<header>`, `<section>`, `<h2>`, `<h3>` -- so screen readers can navigate the structure meaningfully.
**ARIA attributes.** All interactive elements have appropriate ARIA labels, roles, and state attributes. Menus use `role="menu"` with `aria-haspopup` and `aria-expanded`. The progress bar uses `role="progressbar"` with live value updates. The seek feedback overlay uses `aria-live="assertive"` for screen reader announcements.
@@ -273,16 +273,16 @@ TutorialVault is a [Tauri v2](https://v2.tauri.app/) application with a Rust bac
**Frontend (TypeScript + Vite):** A modular architecture with dedicated modules for each concern:
- `main.ts` - Boot sequence, tick loop, keyboard shortcuts, cross-module wiring
- `player.ts` - Video element, seek/volume/speed controls, mute, fullscreen, PiP, error states
- `playlist.ts` - Playlist rendering, tree view, drag-and-drop, search/filter, scroll-to-current
- `subtitles.ts` - Subtitle discovery, menu, embedded track extraction
- `ui.ts` - Zoom, split ratios, notes, info panel, recent menu, toast, collapsible panes
- `tooltips.ts` - Hover tooltips with zoom awareness and viewport clamping
- `store.ts` - Shared state, pure utilities, cross-module callback registry
- `api.ts` - Typed wrapper around Tauri's invoke API
- `main.ts` -- Boot sequence, tick loop, keyboard shortcuts, cross-module wiring
- `player.ts` -- Video element, seek/volume/speed controls, mute, fullscreen, PiP, error states
- `playlist.ts` -- Playlist rendering, tree view, drag-and-drop, search/filter, scroll-to-current
- `subtitles.ts` -- Subtitle discovery, menu, embedded track extraction
- `ui.ts` -- Zoom, split ratios, notes, info panel, recent menu, toast, collapsible panes
- `tooltips.ts` -- Hover tooltips with zoom awareness and viewport clamping
- `store.ts` -- Shared state, pure utilities, cross-module callback registry
- `api.ts` -- Typed wrapper around Tauri's invoke API
**Design theme.** The interface uses a "Cold Open" dark theme built on cool slate backgrounds (`#0f1117` base) with a steel blue accent (`#88A4C4`). Typography uses [Bricolage Grotesque](https://fonts.google.com/specimen/Bricolage+Grotesque) for headings, [Inter](https://fonts.google.com/specimen/Inter) for body text, and [Space Mono](https://fonts.google.com/specimen/Space+Mono) for monospace elements. All fonts are bundled - no external requests at runtime.
**Design theme.** The interface uses a "Cold Open" dark theme built on cool slate backgrounds (`#0f1117` base) with a steel blue accent (`#88A4C4`). Typography uses [Bricolage Grotesque](https://fonts.google.com/specimen/Bricolage+Grotesque) for headings, [Inter](https://fonts.google.com/specimen/Inter) for body text, and [Space Mono](https://fonts.google.com/specimen/Space+Mono) for monospace elements. All fonts are bundled -- no external requests at runtime.
**State is local.** All data is stored in JSON files with atomic writes and backup rotation. No database, no server, no network requests beyond the one-time ffmpeg download.
@@ -296,6 +296,4 @@ No rights reserved. No permission needed. No conditions. No restrictions.
Do whatever you want with it. Learn from it. Change it. Share it. Build on it. The whole point is that it's yours.
---
<sub><i>This project is developed with the help of a locally-run LLM via opencode for scaffolding, planning, and routine code tasks. Architecture and design decisions are my own.</i></sub>
This project is developed with the help of a locally-run LLM via opencode for scaffolding, planning, and routine code tasks. Architecture and design decisions are my own.
@@ -0,0 +1,256 @@
# TutorialVault — 15 UI Enhancements Design
**Goal:** Add 15 real, polished enhancements to the TutorialVault frontend — all WCAG 2.2 AAA compliant.
**Architecture:** All changes are frontend-only (TypeScript + CSS). No Rust/backend changes needed. Each enhancement is independent — no cross-dependencies between them.
**Tech Stack:** TypeScript, CSS, HTML (Tauri v2 webview frontend)
---
## Enhancement 1: Mute Toggle (M key + volume icon click)
**What:** Press `M` to toggle mute. Click the volume icon to toggle mute. Muted state shows `fa-volume-xmark`, grays out the slider. Unmuting restores previous volume.
**Files:** `main.ts`, `player.ts`, `player.css`
**Behavior:**
- Store `lastVolume` before muting. Set volume to 0 on mute.
- On unmute, restore `lastVolume` (default 1.0 if none stored).
- Volume icon dynamically swaps: `fa-volume-high` (>50%), `fa-volume-low` (>0%), `fa-volume-xmark` (0/muted).
- Dragging the volume slider while muted unmutes automatically.
**WCAG:** Volume icon gets `aria-label="Mute"/"Unmute"` dynamically. Muted visual uses icon change + reduced opacity (not color alone).
---
## Enhancement 2: Fullscreen Shortcut (F key)
**What:** Press `F` to toggle fullscreen. Mirrors the existing fullscreen button behavior.
**Files:** `main.ts`
**Behavior:** Add `case 'f':` to the keyboard switch. Calls the same fullscreen toggle as `fsBtn.onclick`.
**WCAG:** No visual changes needed.
---
## Enhancement 3: Seek Feedback Overlay
**What:** Flash `5s` / `+5s` text in the video overlay when seeking via arrow keys. Fades after 600ms.
**Files:** `main.ts`, `player.ts`, `player.css`
**Behavior:**
- New `showSeekFeedback(delta: number)` function in player.ts.
- Reuses the existing video overlay area. Shows text like "5s" or "+5s" centered.
- Uses a CSS class `.seekFeedback` that fades in/out.
- Consecutive presses accumulate: pressing ArrowRight 3 times quickly shows "+15s".
**WCAG:** Overlay area has `aria-live="assertive"` so screen readers announce "Seeked forward 5 seconds". Text meets 7:1 contrast (white on semi-transparent dark).
---
## Enhancement 4: Playlist Search/Filter
**What:** Search input above the playlist. Typing filters by video name or relative path in real-time. Shows "X of Y" while filtering. Clear button (x) resets.
**Files:** `index.html`, `playlist.ts`, `playlist.css`
**Behavior:**
- Add `<input id="plistSearch">` in the playlist `.panelHeader`.
- `renderList()` checks the filter value and skips non-matching items.
- Filter is case-insensitive, matches against `it.title`, `it.name`, `it.relpath`.
- Clear button appears only when input has text.
- Pressing Escape in the search input clears it.
**WCAG:** `aria-label="Search playlist"`. Filtered count via `aria-live="polite"` region. Clear button has 44x44 target and `aria-label="Clear search"`.
---
## Enhancement 5: Scroll-to-Current Button
**What:** Button in playlist header that scrolls the active item into view. Only visible when the active item is off-screen.
**Files:** `index.html`, `playlist.ts`, `playlist.css`
**Behavior:**
- Uses `IntersectionObserver` on the `.row.active` element.
- Button appears (fades in) when active row is not visible.
- Clicking calls `activeRow.scrollIntoView({ block: 'center', behavior: 'smooth' })`.
- Icon: `fa-crosshairs` or `fa-location-dot`.
**WCAG:** `aria-label="Scroll to current video"`. 44x44 touch target. Hidden with `display:none` when not actionable (removed from a11y tree).
---
## Enhancement 6: Picture-in-Picture Button
**What:** PiP toggle button next to the fullscreen button. Uses native `requestPictureInPicture()` API.
**Files:** `index.html`, `player.ts`
**Behavior:**
- New `iconBtn` after fsBtn: `<button class="iconBtn" id="pipBtn">`.
- Icon swaps: `fa-up-right-from-square` (enter PiP) / `fa-down-left-and-up-right-to-center` (exit PiP).
- Listen to `enterpictureinpicture` / `leavepictureinpicture` events to update icon.
- Feature-detect: hide button if `document.pictureInPictureEnabled` is false.
**WCAG:** Dynamic `aria-label="Enter picture-in-picture"/"Exit picture-in-picture"`. Same focus-visible ring as all icon buttons.
---
## Enhancement 7: Timestamp Insertion in Notes
**What:** Button in the notes dock header that inserts `[MM:SS] ` at the textarea cursor position using the current video time.
**Files:** `index.html`, `ui.ts`, `panels.css`
**Behavior:**
- Small button in `#notesHeader`: `<button id="insertTimestamp" aria-label="Insert timestamp">`.
- Icon: `fa-clock` or `fa-stopwatch`.
- On click: read `player.currentTime`, format as `[M:SS]`, insert at `notesBox.selectionStart`.
- After insert, focus the textarea and place cursor after the inserted text.
**WCAG:** `aria-label="Insert timestamp"`. 44x44 target via `::before` expansion. `data-tooltip` for hover discoverability.
---
## Enhancement 8: Keyboard Shortcut Help Panel
**What:** Press `?` to open a help overlay listing all keyboard shortcuts. Press `?` or `Escape` to close.
**Files:** `index.html`, `main.ts`, `main.css` or `components.css`
**Behavior:**
- Hidden `<div id="shortcutHelp" role="dialog" aria-label="Keyboard shortcuts">` in HTML.
- Two-column grid showing key + description pairs.
- Shortcuts shown: Space (play/pause), Left/Right (seek ±5s), Up/Down (volume), M (mute), F (fullscreen), [ / ] (speed), Alt+Arrow (reorder playlist), ? (this help).
- Focus trap inside the dialog while open.
- Semi-transparent dark backdrop.
**WCAG:** `role="dialog"` with `aria-label`. Focus trapped. Escape closes. All text 7:1 contrast. Backdrop doesn't remove background content from a11y tree (uses `aria-hidden="true"` on app root while dialog is open).
---
## Enhancement 9: Playback Speed Shortcuts ([ and ])
**What:** Press `[` to decrease speed one step, `]` to increase speed one step through the SPEEDS array.
**Files:** `main.ts`, `player.ts`
**Behavior:**
- New `cycleSpeed(delta: number)` exported from player.ts.
- Finds current speed in SPEEDS array, moves by delta (clamped to bounds).
- Updates speed button text, gauge icon, and saves to backend.
- Shows toast notification: "Speed: 1.25x".
**WCAG:** Speed change announced via toast (existing `aria-live="polite"` region).
---
## Enhancement 10: Reset Progress Confirmation
**What:** Two-click confirmation before resetting progress. First click changes button to "Confirm?" state with red tint. Second click within 3 seconds actually resets. Timeout or click-away cancels.
**Files:** `ui.ts`, `main.css`
**Behavior:**
- First click: add `.confirming` class, change icon to `fa-exclamation-triangle`, start 3s timer.
- Second click while `.confirming`: execute the actual reset.
- Timer expiry or blur: remove `.confirming`, restore original icon.
- `.confirming` state: red-tinted background (`rgba(255,70,70,.14)`).
**WCAG:** `aria-label` changes to "Confirm reset progress" in confirm state. Visual change uses both color AND icon change (not color alone).
---
## Enhancement 11: Video Load Error State
**What:** When the `<video>` fires an error event, show a centered overlay with warning icon, error message, and "Try next" button.
**Files:** `player.ts`, `player.css`
**Behavior:**
- Listen to `player.addEventListener('error', ...)`.
- Create/show error overlay inside `.videoWrap` with `fa-triangle-exclamation` icon.
- Message: "This file format may not be supported. Try MP4 (H.264/AAC) or WebM."
- "Try next" button calls `nextPrev(+1)`.
- Overlay clears on next successful `loadedmetadata`.
**WCAG:** `role="alert"` for screen reader announcement. "Try next" button has 44x44 target. Error text meets 7:1 contrast.
---
## Enhancement 12: Double-Click to Fullscreen
**What:** Double-clicking the video toggles fullscreen. Single click still toggles play/pause.
**Files:** `player.ts`
**Behavior:**
- Replace simple click handler with a click/double-click discriminator.
- On click: set a 300ms timer. If no second click arrives, toggle play/pause.
- On double-click: cancel the timer, toggle fullscreen instead.
- `dblclick` event on the video overlay area.
**WCAG:** Mouse/touch enhancement only. Fullscreen remains keyboard-accessible via F key and button.
---
## Enhancement 13: Playlist Stats in Header
**What:** Show video count and completion info next to "Playlist" header. E.g., "24 videos · 8 done".
**Files:** `index.html`, `playlist.ts`
**Behavior:**
- Add `<span class="plistStats" id="plistStats"></span>` in the panelHeader.
- In `renderList()`: compute total count, done count, update the span.
- Styled as `--textMuted`, smaller font (11px), monospace.
**WCAG:** Stats text meets 7:1 contrast. Updated via DOM so screen readers can read it on demand.
---
## Enhancement 14: Mini Progress Bars Per Playlist Item
**What:** Thin 2px progress bar at the bottom of each playlist row showing watched/total. Green for done, accent blue for in-progress.
**Files:** `playlist.ts`, `playlist.css`
**Behavior:**
- For each row in `renderList()`, create a `.rowProgress` div.
- Width: `(watched / duration) * 100%`. If duration is null, hide.
- Color: `--success` for done items, `--accent` for in-progress.
- Positioned absolutely at bottom of the row.
**WCAG:** `aria-hidden="true"` — numeric info already in the row's `aria-label`. Colors pass 3:1 minimum for non-text contrast per WCAG 2.2.
---
## Enhancement 15: Collapsible Dock Panes
**What:** Clicking a dock header collapses that pane, giving the other pane full height. Collapsed state shows just the header with a chevron. Persisted via prefs.
**Files:** `ui.ts`, `panels.css`, `index.html`
**Behavior:**
- Click on `.dockHeader` toggles `.collapsed` on the parent `.dockPane`.
- Collapsed: `flex:0 0 auto`, content inside has `display:none`.
- Chevron icon in header rotates to indicate collapsed/expanded.
- Collapsing one pane doesn't collapse the other.
- State saved in prefs as `notes_collapsed` / `info_collapsed` booleans.
**WCAG:** Headers get `aria-expanded="true"/"false"`. Chevron is `aria-hidden="true"`. Enter/Space on header toggles. Collapsed content is `display:none` (removed from a11y tree).
---
## Approach
**Single-pass, all frontend:** Every enhancement modifies only TypeScript and CSS files. No backend/Rust changes. Each enhancement is independent and can be implemented in any order.
**Implementation strategy:** Direct edits to existing files using Edit/Write tools. No subagents for file editing. Each enhancement touches 1-3 files. Build verification after each enhancement or batch.
**Testing:** `npm run build` (TypeScript compilation + Vite build) after each enhancement to catch errors immediately.
+184
View File
@@ -0,0 +1,184 @@
# WCAG 2.2 AAA Remediation Design
**Goal:** Bring TutorialVault to full WCAG 2.2 AAA compliance using surgical, in-place edits that preserve the Cold Open aesthetic.
**Approach:** Edit existing files directly. No new modules or abstractions. Fix each audit finding with the minimum code change. Contrast values raised just enough to hit 7:1 while keeping the cool, muted hierarchy.
**Constraints:**
- Preserve the Cold Open visual identity (dark slate, steel blue accent, borderless surfaces)
- Raise muted text contrast minimally to hit AAA 7:1
- Focus rings use the existing accent color, subtle but compliant
- No new dependencies
---
## 1. Semantic HTML & Landmarks
**File: `src/index.html`**
- Add `lang="en"` to `<html>`
- Wrap `.topbar` in `<header role="banner">`
- Wrap `.content` in `<main role="main">`
- Add `role="region" aria-label="Video player"` to the left `.panel`
- Add `role="region" aria-label="Playlist"` to the right `.panel`
- Add `role="complementary" aria-label="Details"` to the `.dock`
- Change `.appName` from `<div>` to `<h1>` (keep existing class)
- Change `.nowTitle` from `<div>` to `<h2>` (keep existing class)
- Change `.dockTitle` elements from `<div>` to `<h3>` (keep existing class)
- Change `.playlistHeader` from `<div>` to `<h2>` (keep existing class)
- Convert info panel `.kv` blocks from nested `<div>` to `<dl>`/`<dt>`/`<dd>`, styled with same `.kv`/`.k`/`.v` classes
No visual change. Headings inherit their existing class styles. Landmarks are invisible.
---
## 2. Text Alternatives & ARIA
### Icon-only buttons get `aria-label`
| Button | `aria-label` |
|---|---|
| `zoomOutBtn` | "Zoom out" |
| `zoomInBtn` | "Zoom in" |
| `zoomResetBtn` (change `<span>` to `<button>`) | "Reset zoom" |
| `chooseDropBtn` | "Recent folders" |
| `resetProgBtn` | "Reset progress" |
| `refreshBtn` | "Reload folder" |
| `winMinBtn` | "Minimize" |
| `winMaxBtn` | "Maximize" |
| `winCloseBtn` | "Close" |
| `prevBtn` | "Previous video" |
| `playPauseBtn` | "Play" (dynamically toggled to "Pause") |
| `nextBtn` | "Next video" |
| `subsBtn` | "Subtitles" |
| `fsBtn` | "Toggle fullscreen" |
### Other ARIA additions
- Add `aria-hidden="true"` to all decorative `<i>` icons inside labeled buttons
- Add `aria-label="Video player"` to `<video>`
- Add `role="progressbar"`, `aria-valuenow`, `aria-valuemin="0"`, `aria-valuemax="100"`, `aria-label="Overall folder progress"` to `.progressBar`. Update dynamically in `updateOverall()`
- Add `aria-label="Volume"` to volume slider
- Toggle `aria-expanded="true"/"false"` on `subsBtn`, `speedBtn`, `chooseDropBtn` when menus open/close
- Toast already has `aria-live="polite"` -- no change needed
### Playlist rows (in `playlist.ts`)
- Set `role="listbox"` and `aria-label="Playlist"` on the `#list` container
- Each row: `role="option"`, `aria-selected="true"/"false"`, computed `aria-label` (e.g. "03. Video Title - 5:30 / 12:00 - Done")
---
## 3. Color Contrast (AAA 7:1)
Targeted CSS custom property adjustments in `:root`. Hierarchy preserved, just shifted up.
| Token | Current value | New value | Ratio vs #151821 |
|---|---|---|---|
| `--textMuted` | `rgba(148,162,192,.55)` ~3.6:1 | `rgba(160,174,204,.72)` ~7.2:1 | AAA pass |
| `--textDim` | `rgba(118,132,168,.38)` ~1.8:1 | `rgba(158,174,208,.68)` ~7.1:1 | AAA pass |
| `.tagline` color | `rgba(148,162,192,.62)` ~3.7:1 | Use `--textMuted` | AAA pass |
| `.notes::placeholder` | `rgba(148,162,192,.40)` ~2.2:1 | `rgba(155,170,200,.65)` ~5.5:1 | Exempt but improved |
| Time `/` separator | `rgba(165,172,196,.65)` | `rgba(175,185,210,.78)` ~7.1:1 | AAA pass |
| `--icon` | `rgba(148,162,195,.48)` | `rgba(160,175,210,.62)` | Non-text 3:1 pass |
---
## 4. Focus Indicators (AAA 2.4.13)
Global `:focus-visible` rule in `main.css`:
```css
*:focus-visible {
outline: 2px solid rgba(136,164,196,.65);
outline-offset: 2px;
border-radius: inherit;
}
```
Component refinements:
- Buttons: outline around button shape (default behavior)
- Playlist rows: `outline-offset: -2px` to stay within bounds
- Sliders: `:focus-visible::-webkit-slider-thumb` box-shadow glow
- Notes textarea: enhance existing `:focus` border to 3:1 contrast
- Switch labels: outline around entire label
- Menu items: background highlight + outline on focus
`rgba(136,164,196,.65)` vs `#151821` gives ~5.5:1 -- passes the 3:1 requirement for focus indicators.
---
## 5. Keyboard Accessibility
### Interactive `<div>` elements become keyboard-accessible
1. **Playlist rows**: `tabindex="0"`, `keydown` Enter/Space to activate, Arrow Up/Down to navigate rows
2. **Menu items** (subtitle, speed, recent): Use `<button>` elements or `tabindex="0"` with `role="menuitem"`. Enter/Space to activate, Arrow Up/Down to navigate, Escape to close and return focus to trigger
3. **`zoomResetBtn`**: Change from `<span>` to `<button>`
4. **Resize dividers**: `tabindex="0"`, `role="separator"`, `aria-valuenow`, `aria-orientation="vertical"`. Arrow Left/Right to resize in 2% increments
### Menu keyboard pattern (subtitles, speed, recent)
- Trigger button: `aria-haspopup="true"`, `aria-expanded="false"/"true"`
- On open: focus moves to first menu item
- Arrow Up/Down: navigate items
- Enter/Space: activate item
- Escape: close menu, return focus to trigger
- Tab: close menu
### Playlist keyboard reorder (WCAG 2.5.7)
- Focused row: Alt+ArrowUp / Alt+ArrowDown moves it
- Small move-up/move-down `<button>` elements appear on row focus/hover (right side, before tag)
- After move, focus follows the moved row to its new position
- A live region announces "Moved to position X"
---
## 6. Remaining AAA Criteria
### Dynamic page title (2.4.2)
Update `document.title` in `loadIndex()` and `onLibraryLoaded()`:
- Playing: `"Video Title - TutorialVault"`
- No video: `"TutorialVault - Open a folder"`
### Abbreviations (3.1.4)
Wrap abbreviations in info panel with `<abbr title="...">`:
- ETA -> `<abbr title="Estimated Time of Arrival">ETA</abbr>`
- FPS -> `<abbr title="Frames Per Second">FPS</abbr>`
- kbps/Mbps -> `<abbr title="kilobits per second">kbps</abbr>`
Update `updateInfoPanel()` and `refreshCurrentVideoMeta()` in `ui.ts`.
### Target size (2.5.5 AAA -- 44x44px)
Expand hit areas of small buttons using padding + negative margin:
- `.zoomBtn` (28x28): add padding to reach 44x44 effective, negative margin to keep visual layout
- `.winBtn` (30x30): same technique
- `.dropRemove` (24x24): same technique
Visual size stays the same; click/touch area grows.
### Reduced motion (2.3.3)
Already handled -- `prefers-reduced-motion` media query disables all animations/transitions. No change needed.
---
## Files Modified
| File | Changes |
|---|---|
| `src/index.html` | `lang`, landmarks, headings, `aria-label`s, `aria-hidden`, `<button>` for zoomReset, `<dl>` for info panel, `role`/`aria` on progress bar and video |
| `src/styles/main.css` | `:root` contrast values, `:focus-visible` rules, hit area expansion for small buttons |
| `src/styles/player.css` | Focus styles for sliders and control buttons |
| `src/styles/playlist.css` | Focus styles for rows, move button styling |
| `src/styles/panels.css` | Focus styles for notes, divider roles |
| `src/styles/components.css` | Focus styles for menu items, tooltip improvements |
| `src/playlist.ts` | `role="listbox"`, row `role="option"`, `tabindex`, keyboard nav, move buttons, Alt+Arrow reorder, live region |
| `src/player.ts` | `aria-expanded` on speed button, menu keyboard nav, dynamic `aria-label` on play/pause, Escape handler |
| `src/subtitles.ts` | `aria-expanded` on subs button, menu items as buttons, keyboard nav, Escape handler |
| `src/ui.ts` | `aria-expanded` on recent dropdown, menu keyboard nav, Escape handler, divider keyboard resize, progress bar ARIA updates, dynamic `document.title`, abbreviation wrapping |
| `src/main.ts` | Dynamic `document.title` updates |
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -964,7 +964,7 @@ impl Library {
// -----------------------------------------------------------------------
/// Build a protocol URL for a stored subtitle VTT path.
/// The `vtt` field is like `"subtitles/{fid}_{name}.vtt"` - extract the filename.
/// The `vtt` field is like `"subtitles/{fid}_{name}.vtt"` extract the filename.
fn _sub_url(vtt: &str) -> String {
let filename = vtt.rsplit('/').next().unwrap_or(vtt);
format!("http://tutdock.localhost/sub/{}", filename)
+1 -1
View File
@@ -36,7 +36,7 @@ fn main() {
// 4. Load preferences.
let prefs_data = prefs::Prefs::load(&state_dir);
// 5. Initialize library (empty - loaded from last folder or frontend action).
// 5. Initialize library (empty loaded from last folder or frontend action).
let mut lib = library::Library::new();
// Discover ffmpeg/ffprobe.
+2 -2
View File
@@ -364,7 +364,7 @@ mod tests {
prefs.window.x = Some(50);
prefs.window.y = Some(60);
// Patch only width - height, x, y should remain
// Patch only width height, x, y should remain
let patch = json!({
"window": {"width": 1600}
});
@@ -395,7 +395,7 @@ mod tests {
#[test]
fn test_load_with_missing_fields_uses_defaults() {
let dir = TempDir::new().unwrap();
// Write a JSON with only version - everything else should be defaults
// Write a JSON with only version everything else should be defaults
let partial = json!({"version": 20});
let path = dir.path().join("prefs.json");
atomic_write_json(&path, &partial, BACKUP_COUNT);
+1 -1
View File
@@ -127,7 +127,7 @@ mod tests {
let dir = TempDir::new().unwrap();
push_recent(dir.path(), "/folder/a");
push_recent(dir.path(), "/folder/b");
push_recent(dir.path(), "/folder/a"); // duplicate - should move to front
push_recent(dir.path(), "/folder/a"); // duplicate should move to front
let recents = load_recents(dir.path());
assert_eq!(recents.len(), 2);
+2 -2
View File
@@ -124,9 +124,9 @@ mod tests {
let first = json!({"version": 1});
let second = json!({"version": 2});
// First write - creates primary + lastgood
// First write creates primary + lastgood
atomic_write_json(&path, &first, BACKUP_COUNT);
// Second write - rotates first to .bak1, writes second as primary + lastgood
// Second write rotates first to .bak1, writes second as primary + lastgood
atomic_write_json(&path, &second, BACKUP_COUNT);
// Corrupt primary and lastgood
+7 -7
View File
@@ -66,7 +66,7 @@ static NORMALIZE_SEP_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"[-_\s]+").unwra
/// - Removes BOM (`\u{FEFF}`) if present.
/// - Adds the `WEBVTT` header.
/// - Skips cue index numbers (lines that are just digits).
/// - Converts timestamp separators: comma -> dot.
/// - Converts timestamp separators: comma dot.
/// - Collects subtitle text between timestamp lines and empty lines.
pub fn srt_to_vtt(srt_text: &str) -> String {
let text = srt_text.replace('\u{FEFF}', "");
@@ -77,7 +77,7 @@ pub fn srt_to_vtt(srt_text: &str) -> String {
while i < lines.len() {
let line = lines[i].trim_end_matches('\r');
// Empty line -> blank line in output
// Empty line blank line in output
if line.trim().is_empty() {
out.push(String::new());
i += 1;
@@ -149,7 +149,7 @@ fn normalize_stem(s: &str) -> String {
/// Strip a trailing language suffix from a subtitle stem.
///
/// For example, `"video.en"` -> `Some(("video", "en"))`.
/// For example, `"video.en"` `Some(("video", "en"))`.
/// Returns `None` if no known language suffix is found.
fn strip_lang_suffix(stem: &str) -> Option<(String, String)> {
if let Some(dot_pos) = stem.rfind('.') {
@@ -297,7 +297,7 @@ fn sanitize_name(name: &str) -> String {
}
}
/// Store a subtitle file for a given fid. Converts SRT->VTT if needed.
/// Store a subtitle file for a given fid. Converts SRTVTT if needed.
///
/// The output file is written as `{fid}_{sanitized_name}.vtt` inside
/// `subs_dir`. Returns `SubtitleStored` with the relative path (from the
@@ -341,7 +341,7 @@ pub fn store_subtitle_for_fid(
let vtt_content = if ext_lower == ".srt" {
srt_to_vtt(&content)
} else {
// Already VTT - use as-is.
// Already VTT use as-is.
content
};
@@ -573,13 +573,13 @@ Second line
// Should pick priority 0 (exact match) over others.
assert_eq!(result.unwrap(), exact);
// Remove exact match -> should pick English suffix (priority 2).
// Remove exact match should pick English suffix (priority 2).
fs::remove_file(&exact).unwrap();
let result = auto_subtitle_sidecar(&video);
assert!(result.is_some());
assert_eq!(result.unwrap(), en_suffix);
// Remove English suffix -> should pick French suffix (priority 4).
// Remove English suffix should pick French suffix (priority 4).
fs::remove_file(&en_suffix).unwrap();
let result = auto_subtitle_sidecar(&video);
assert!(result.is_some());
+7 -7
View File
@@ -5,12 +5,12 @@
//! On macOS / Linux it would be `tutdock://localhost/<path>`.
//!
//! Routes:
//! /video/{index} - video file with Range support
//! /sub/{libid}/{fid} - stored VTT subtitle
//! /fonts.css - cached Google Fonts CSS
//! /fonts/{filename} - cached font file
//! /fa.css - cached Font Awesome CSS
//! /fa/webfonts/{filename} - cached FA webfont
//! /video/{index} video file with Range support
//! /sub/{libid}/{fid} stored VTT subtitle
//! /fonts.css cached Google Fonts CSS
//! /fonts/{filename} cached font file
//! /fa.css cached Font Awesome CSS
//! /fa/webfonts/{filename} cached FA webfont
use std::fs::{self, File};
use std::io::{Read, Seek, SeekFrom};
@@ -75,7 +75,7 @@ fn handle_request(
}
// ---------------------------------------------------------------------------
// Video - with Range request support
// Video with Range request support
// ---------------------------------------------------------------------------
fn handle_video(
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* TutorialDock frontend - boot sequence, tick loop, global wiring.
* TutorialDock frontend boot sequence, tick loop, global wiring.
* Orchestrates all modules and holds cross-module callbacks.
*/
import '@fortawesome/fontawesome-free/css/all.min.css';
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Video playback controls - manages the <video> element, seek bar,
* Video playback controls manages the <video> element, seek bar,
* volume slider, play/pause, fullscreen, and video overlay.
*/
import { api } from './api';
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Playlist rendering - list items, tree SVG connectors, custom scrollbar,
* Playlist rendering list items, tree SVG connectors, custom scrollbar,
* and drag-and-drop reorder.
*/
import { api } from './api';
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* Shared mutable state and pure utility functions.
* All modules import from here - no circular dependencies.
* All modules import from here no circular dependencies.
*/
import type { LibraryInfo, VideoItem } from './types';
+1 -1
View File
@@ -1,4 +1,4 @@
/* Reduced motion - respect user preference */
/* Reduced motion respect user preference */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
+1 -1
View File
@@ -38,7 +38,7 @@
color:var(--text);
}
/* Toolbar icon buttons - borderless */
/* Toolbar icon buttons borderless */
.toolbarIcon{
width:36px; height:36px;
border-radius:var(--r2);
+15 -15
View File
@@ -1,18 +1,18 @@
:root{
--zoom:1;
/* Type scale - Minor Third (1.2) from 13px base: 10 · 11 · 12 · 13 · 15 · 17 · 19 */
/* Base backgrounds - cool dark slate, lighter */
/* Type scale Minor Third (1.2) from 13px base: 10 · 11 · 12 · 13 · 15 · 17 · 19 */
/* Base backgrounds cool dark slate, lighter */
--bg0:#0f1117; --bg1:#151821;
/* Strokes - cool, very subtle */
/* Strokes cool, very subtle */
--stroke:rgba(140,160,210,.07);
--strokeLight:rgba(140,160,210,.04);
--strokeMed:rgba(140,160,210,.09);
--strokeStrong:rgba(140,160,210,.14);
/* Text - cool white hierarchy */
/* Text cool white hierarchy */
--text:rgba(218,225,240,.90);
--textMuted:rgba(185,196,222,.86);
--textDim:rgba(172,184,214,.88);
/* Surfaces - cool-tinted, subtle fills */
/* Surfaces cool-tinted, subtle fills */
--surface-0:rgba(140,165,220,.04);
--surface:rgba(140,165,220,.06);
--surface-2:rgba(140,165,220,.09);
@@ -20,15 +20,15 @@
--surface-4:rgba(140,165,220,.15);
--surfaceHover:rgba(140,165,220,.10);
--surfaceActive:rgba(140,165,220,.13);
/* Shadows - minimal */
/* Shadows minimal */
--shadow:0 8px 24px rgba(0,0,0,.25);
--shadow2:0 4px 12px rgba(0,0,0,.15);
--shadow3:none;
--shadowFloat:0 6px 20px rgba(0,0,0,.20);
--shadowInset:inset 0 1px 3px rgba(0,0,0,.12);
/* Radii - architectural */
/* Radii architectural */
--r:6px; --r2:4px; --r3:3px;
/* Easing - refined, springy */
/* Easing refined, springy */
--ease-spring:cubic-bezier(.25,.46,.45,.94);
--ease-bounce:cubic-bezier(.34,1.56,.64,1);
--ease-out-back:cubic-bezier(.34,1.3,.64,1);
@@ -36,19 +36,19 @@
--mono:"Space Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
--sans:"Inter", ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
--brand:"Bricolage Grotesque", "Inter", ui-sans-serif, system-ui, sans-serif;
/* Icons - cool */
/* Icons cool */
--icon:rgba(160,175,210,.62);
--iconStrong:rgba(200,212,238,.75);
/* Accent - steel blue */
/* Accent steel blue */
--accent:#88A4C4;
--accentGlow:rgba(136,164,196,.08);
--accentBorder:rgba(136,164,196,.18);
--accentBg:rgba(136,164,196,.06);
/* Success - muted sage */
/* Success muted sage */
--success:rgb(130,170,130);
--successBg:rgba(130,170,130,.06);
--successBorder:rgba(130,170,130,.15);
/* Tree - cool */
/* Tree cool */
--tree:rgba(140,165,220,.07);
--treeNode:rgba(200,212,238,.40);
}
@@ -237,7 +237,7 @@ body{
transition:background .3s ease;
}
/* Zoom control - borderless */
/* Zoom control borderless */
.zoomControl{
display:flex; align-items:center; gap:0;
background:var(--surface-2);
@@ -294,7 +294,7 @@ body{
transition-duration:.08s;
}
/* Toolbar buttons - borderless */
/* Toolbar buttons borderless */
.toolbarBtn{
width:34px; height:34px;
border:none;
@@ -336,7 +336,7 @@ body{
}
.windowControls{gap:4px; margin-left:2px;}
/* Primary split button - borderless */
/* Primary split button borderless */
.splitBtn.primary{
background:rgba(136,164,196,.10);
}
+2 -2
View File
@@ -57,7 +57,7 @@
background:rgba(136,164,196,.30);
}
/* Row - no transform/padding changes (preserves drag-and-drop) */
/* Row no transform/padding changes (preserves drag-and-drop) */
.row{position:relative; display:flex; align-items:flex-start; justify-content:space-between; gap:10px; padding:9px 12px; border-bottom:1px solid var(--strokeLight); cursor:pointer; user-select:none; transition:background .2s ease, box-shadow .2s ease; box-shadow:inset 3px 0 0 transparent;}
.row:hover{background:var(--surfaceHover); box-shadow:inset 3px 0 0 rgba(136,164,196,.40);}
.row:active{background:var(--surfaceActive);}
@@ -69,7 +69,7 @@
.numBadge{flex:0 0 auto; min-width:38px; height:22px; padding:0 8px; border-radius:var(--r2); border:none; background:var(--surface-2); box-shadow:var(--shadow3); display:flex; align-items:center; justify-content:center; font-family:var(--mono); font-size:12px; letter-spacing:.02em; color:var(--text); font-variant-numeric:tabular-nums; margin-top:1px; transition:all .2s var(--ease-bounce);}
.row:hover .numBadge{background:var(--surface-3); transform:scale(1.06);}
/* Tree connectors - fully static, opaque, dark lines */
/* Tree connectors fully static, opaque, dark lines */
.treeSvg{flex:0 0 auto; margin-top:1px; overflow:visible;}
.treeSvg line{stroke:rgb(40,46,60); stroke-width:1.5;}
.treeSvg circle{fill:rgba(200,212,238,.60); stroke:rgba(136,164,196,.18); stroke-width:1;}
+2 -2
View File
@@ -1,5 +1,5 @@
/**
* UI controls - zoom, split ratios, topbar, recent menu, info panel,
* UI controls zoom, split ratios, topbar, recent menu, info panel,
* notes, toast notifications, and reset/reload buttons.
*/
import { api } from './api';
@@ -322,7 +322,7 @@ export function initUI(): void {
}, 3000);
return;
}
// Second click - actually reset
// Second click actually reset
cancelResetConfirm();
try {
const res = await api.resetWatchProgress();