Compare commits

..

24 Commits

Author SHA1 Message Date
lashman f94b50d205 minor README edits 2026-03-13 18:24:53 +02:00
lashman d993b8610b clean up README list formatting 2026-02-19 18:29:29 +02:00
lashman 4fddb02336 WCAG AAA contrast, speed menu z-index, custom app icon 2026-02-19 18:23:38 +02:00
lashman 2a173c1dff update README 2026-02-19 17:10:33 +02:00
lashman 6c02465185 update fonts, Tauri config, and reduced-motion support 2026-02-19 17:03:39 +02:00
lashman e5265a970d add 15 UI enhancements 2026-02-19 17:01:01 +02:00
lashman ac07f69ace remove controls container, bump surface contrast 2026-02-19 16:54:12 +02:00
lashman e0faae61e8 reset heading margins and button chrome 2026-02-19 16:40:39 +02:00
lashman dc6942b504 WCAG 2.2 AAA compliance pass 2026-02-19 16:35:19 +02:00
lashman f938be82bd expand README philosophy section 2026-02-19 12:58:55 +02:00
lashman f05aedf6bc add README badges 2026-02-19 12:54:43 +02:00
lashman 4b39cf751a add emoji to README 2026-02-19 12:53:50 +02:00
lashman cc6df37309 add README 2026-02-19 12:52:21 +02:00
lashman 1d6d761a63 complete Tauri v2 port with runtime fixes 2026-02-19 12:44:57 +02:00
lashman 61379d9b64 add all frontend TypeScript modules 2026-02-19 11:44:48 +02:00
lashman 28cc8491c2 add video_protocol.rs, commands.rs, main.rs, and index.html 2026-02-19 11:23:37 +02:00
lashman e5f5f7985b add library.rs, types.ts, api.ts, and CSS 2026-02-19 02:08:23 +02:00
lashman ffa37e6061 add ffmpeg.rs, subtitles.rs, and fonts.rs 2026-02-19 01:59:21 +02:00
lashman 4fd04f55c8 add recents.rs 2026-02-19 01:50:28 +02:00
lashman c0c3a21bd5 add prefs.rs 2026-02-19 01:50:20 +02:00
lashman 5dd817cf37 add utils.rs and state.rs 2026-02-19 01:47:37 +02:00
lashman 0b7532acd3 remove protocol-asset feature, add app icons 2026-02-19 01:29:50 +02:00
lashman 768957103c scaffold Tauri v2 + Vite + TypeScript project 2026-02-19 01:23:27 +02:00
lashman dd9a8082d9 existing TutorialDock Python app 2026-02-19 01:16:17 +02:00
20 changed files with 57 additions and 1653 deletions
+11 -13
View File
@@ -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.
@@ -295,5 +295,3 @@ TutorialVault is released into the public domain under [CC0 1.0 Universal](https
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.
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.
@@ -1,256 +0,0 @@
# 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
@@ -1,184 +0,0 @@
# 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 SRTVTT if needed.
/// Store a subtitle file for a given fid. Converts SRT->VTT 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();