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

View File

@@ -282,7 +282,7 @@ TutorialVault is a [Tauri v2](https://v2.tauri.app/) application with a Rust bac
- `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 [Fraunces](https://fonts.google.com/specimen/Fraunces) 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.

10
package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "tutorialvault",
"version": "0.1.0",
"dependencies": {
"@fontsource/fraunces": "^5.2.9",
"@fontsource/bricolage-grotesque": "^5.2.10",
"@fontsource/inter": "^5.2.8",
"@fontsource/space-mono": "^5.2.9",
"@fortawesome/fontawesome-free": "^7.2.0",
@@ -411,10 +411,10 @@
"node": ">=12"
}
},
"node_modules/@fontsource/fraunces": {
"version": "5.2.9",
"resolved": "https://registry.npmjs.org/@fontsource/fraunces/-/fraunces-5.2.9.tgz",
"integrity": "sha512-XDzuddBtoC7BZgZdBn6b7hsFZY2+V1hgN7yca5fBTKuHjb/lOd45a0Ji8dTUgFhPoL7RdGupo+bC2BFSt6UH8Q==",
"node_modules/@fontsource/bricolage-grotesque": {
"version": "5.2.10",
"resolved": "https://registry.npmjs.org/@fontsource/bricolage-grotesque/-/bricolage-grotesque-5.2.10.tgz",
"integrity": "sha512-V2xS+1P7C8IrSypXLUx/bLtX/LsTlYtV2k2CsU+S/0t8qepZ2hvKSlyJIx7Ub/iY8Bbnj+IjAuUF9nvFz+BbIg==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"

View File

@@ -9,7 +9,7 @@
"tauri": "tauri"
},
"dependencies": {
"@fontsource/fraunces": "^5.2.9",
"@fontsource/bricolage-grotesque": "^5.2.10",
"@fontsource/inter": "^5.2.8",
"@fontsource/space-mono": "^5.2.9",
"@fortawesome/fontawesome-free": "^7.2.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 882 B

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 138 KiB

View File

@@ -3,9 +3,9 @@
* Orchestrates all modules and holds cross-module callbacks.
*/
import '@fortawesome/fontawesome-free/css/all.min.css';
import '@fontsource/fraunces/600.css';
import '@fontsource/fraunces/700.css';
import '@fontsource/fraunces/800.css';
import '@fontsource/bricolage-grotesque/500.css';
import '@fontsource/bricolage-grotesque/600.css';
import '@fontsource/bricolage-grotesque/700.css';
import '@fontsource/inter/400.css';
import '@fontsource/inter/500.css';
import '@fontsource/inter/600.css';
@@ -171,7 +171,7 @@ async function tick(): Promise<void> {
try { await api.tickProgress(currentIndex, t, d, playing); } catch (_) {}
}
if (now % 3000 < 1000) {
if (now % 3000 < 1000 && !suppressTick) {
try {
const info = await api.getLibrary();
if (info && info.ok) {

View File

@@ -155,6 +155,12 @@ export function initPlayer(): void {
if (d > 0) seek.value = String(Math.round((t / d) * 1000));
updateSeekFill();
updateTimeReadout();
// Update active row's mini progress bar in real time
const activeRow = document.querySelector('.row.active');
if (activeRow && d > 0) {
const bar = activeRow.querySelector('.rowProgress') as HTMLElement;
if (bar) bar.style.width = clamp((t / d) * 100, 0, 100) + '%';
}
}
cb.updateInfoPanel?.();
});
@@ -434,8 +440,8 @@ export function cycleSpeed(delta: number): void {
cb.updateInfoPanel?.();
cb.notify?.(`Speed: ${r}x`);
setSuppressTick(true);
if (library) { api.setFolderRate(r).catch(() => {}); }
setSuppressTick(false);
if (library) { api.setFolderRate(r).catch(() => {}).finally(() => setSuppressTick(false)); }
else { setSuppressTick(false); }
}
/** Load a video by index and handle the onloadedmetadata callback. */
@@ -530,7 +536,7 @@ export function buildSpeedMenu(active: number): void {
left.appendChild(dot); left.appendChild(txt);
row.appendChild(left);
row.onclick = async () => {
row.onclick = () => {
closeSpeedMenu();
const r = clamp(Number(s), 0.25, 3);
player.playbackRate = r;
@@ -540,8 +546,8 @@ export function buildSpeedMenu(active: number): void {
buildSpeedMenu(r);
cb.updateInfoPanel?.();
setSuppressTick(true);
if (library) { try { await api.setFolderRate(r); } catch (_) {} }
setSuppressTick(false);
if (library) { api.setFolderRate(r).catch(() => {}).finally(() => setSuppressTick(false)); }
else { setSuppressTick(false); }
};
speedMenu.appendChild(row);
}

View File

@@ -33,7 +33,7 @@
}
.toastMsg{
font-size:13px;
font-weight:600;
font-weight:500;
letter-spacing:0;
color:var(--text);
}
@@ -82,17 +82,17 @@
.tooltip::after{display:none;}
.tooltip-title{
font-family:var(--brand);
font-weight:700;
font-weight:600;
font-size:14px;
margin-bottom:6px;
letter-spacing:-.01em;
letter-spacing:-.02em;
color:rgba(235,240,252,.95);
}
.tooltip-desc{
font-family:var(--sans);
font-size:12px;
font-weight:500;
color:rgba(170,182,210,.82);
font-weight:400;
color:rgba(170,182,210,.86);
line-height:1.55;
letter-spacing:0;
position:relative;
@@ -115,7 +115,7 @@
}
.subsMenu.show{display:block; opacity:1; transform:translateX(-50%) scale(1);}
.subsMenuHeader{padding:6px 12px 4px; font-size:10px; font-weight:600; text-transform:uppercase; letter-spacing:.08em; color:var(--textDim); transition:color .2s ease;}
.subsMenuItem{padding:10px 12px; border-radius:var(--r3); cursor:pointer; user-select:none; font-size:12px; font-weight:600; color:var(--text); letter-spacing:0; display:flex; align-items:center; gap:10px; transition:all .2s var(--ease-bounce);}
.subsMenuItem{padding:10px 12px; border-radius:var(--r3); cursor:pointer; user-select:none; font-size:12px; font-weight:500; color:var(--text); letter-spacing:0; display:flex; align-items:center; gap:10px; transition:all .2s var(--ease-bounce);}
.subsMenuItem:hover{background:var(--surfaceHover); padding-left:16px;}
.subsMenuItem:active{transform:scale(.97); transition-duration:.08s;}
.subsMenuItem .fa{font-size:13px; color:var(--iconStrong)!important; opacity:.85; width:18px; text-align:center; transition:transform .2s var(--ease-bounce), opacity .15s ease;}
@@ -175,11 +175,12 @@
}
.shortcutHelpTitle{
font-family:var(--brand);
font-weight:700;
font-size:16px;
font-weight:600;
font-size:17px;
line-height:1.2;
margin:0 0 20px;
color:rgba(235,240,252,.95);
letter-spacing:-.01em;
letter-spacing:-.02em;
}
.shortcutGrid{
display:grid;

View File

@@ -1,5 +1,6 @@
: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 */
--bg0:#0f1117; --bg1:#151821;
/* Strokes — cool, very subtle */
@@ -9,8 +10,8 @@
--strokeStrong:rgba(140,160,210,.14);
/* Text — cool white hierarchy */
--text:rgba(218,225,240,.90);
--textMuted:rgba(160,174,204,.72);
--textDim:rgba(158,174,208,.68);
--textMuted:rgba(185,196,222,.86);
--textDim:rgba(172,184,214,.88);
/* Surfaces — cool-tinted, subtle fills */
--surface-0:rgba(140,165,220,.04);
--surface:rgba(140,165,220,.06);
@@ -34,7 +35,7 @@
/* Fonts */
--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:"Fraunces", Georgia, "Times New Roman", serif;
--brand:"Bricolage Grotesque", "Inter", ui-sans-serif, system-ui, sans-serif;
/* Icons — cool */
--icon:rgba(160,175,210,.62);
--iconStrong:rgba(200,212,238,.75);
@@ -58,7 +59,7 @@ body{
margin:0; padding:0; font-family:var(--sans); color:var(--text); overflow:hidden;
width:100vw; height:100vh;
font-weight:400;
line-height:1.4;
line-height:1.45;
background:
radial-gradient(900px 600px at 8% 3%, rgba(100,140,210,.04), transparent 55%),
radial-gradient(600px 400px at 92% 97%, rgba(90,120,180,.03), transparent 60%),
@@ -197,9 +198,9 @@ body{
.brandText{min-width:0; position:relative; z-index:1;}
.appName{
font-family:var(--brand);
font-weight:800;
font-size:18px;
line-height:1.02;
font-weight:700;
font-size:19px;
line-height:1.1;
letter-spacing:-.02em;
margin:0; padding:0;
transition:text-shadow .3s var(--ease-spring), color .2s ease;
@@ -218,7 +219,7 @@ body{
transition:color .3s var(--ease-spring);
}
.brand:hover .tagline{
color:rgba(180,192,218,.82);
color:rgba(192,202,226,.90);
}
.actions{
@@ -429,7 +430,7 @@ body{
.dropdownPortal::-webkit-scrollbar-thumb{background:rgba(140,160,210,.10); border-radius:999px;}
.dropdownPortal::-webkit-scrollbar-button{width:0; height:0; display:none;}
.dropItem{display:flex; align-items:center; gap:10px; padding:10px 12px; border-radius:var(--r3); cursor:pointer; user-select:none; color:var(--text); font-weight:600; font-size:13px; letter-spacing:0; line-height:1.25; transition:all .2s var(--ease-bounce); position:relative;}
.dropItem{display:flex; align-items:center; gap:10px; padding:10px 12px; border-radius:var(--r3); cursor:pointer; user-select:none; color:var(--text); font-weight:500; font-size:13px; letter-spacing:0; line-height:1.25; transition:all .2s var(--ease-bounce); position:relative;}
.dropItem:hover{background:var(--surfaceHover); padding-left:16px; padding-right:36px;}
.dropItem:active{transform:scale(.98); transition-duration:.08s;}
.dropIcon{width:18px; height:18px; display:flex; align-items:center; justify-content:center; flex:0 0 auto; opacity:.9; transition:transform .25s var(--ease-bounce), opacity .15s ease;}
@@ -446,7 +447,7 @@ body{
.seg{display:inline-flex; border:none; border-radius:var(--r2); overflow:hidden; background:var(--surface-2); transition:background .15s ease;}
.seg:hover{background:var(--surface-3);}
.seg .btn{border:none; box-shadow:none; border-radius:0; padding:8px 9px; background:transparent; font-weight:800; transform:none;}
.seg .btn{border:none; box-shadow:none; border-radius:0; padding:8px 9px; background:transparent; font-weight:700; transform:none;}
.seg .btn:hover{background:var(--surface-3); transform:none; box-shadow:none;}
.seg .btn:active{background:var(--surface-4); transform:scale(.95);}
.seg .mid{border-left:1px solid rgba(140,160,210,.06); border-right:1px solid rgba(140,160,210,.06); min-width:62px; font-variant-numeric:tabular-nums;}

View File

@@ -4,9 +4,9 @@
.panelHeader{padding:10px 12px 8px; border-bottom:1px solid rgba(140,160,210,.04); display:flex; align-items:flex-start; justify-content:space-between; gap:12px; flex:0 0 auto; min-width:0; background:rgba(140,165,220,.015); transition:background .3s ease;}
.panelHeader:hover{background:rgba(140,165,220,.025);}
.panelHeader::after{display:none;}
.nowTitle{font-family:var(--brand); font-weight:700; font-size:14px; letter-spacing:-.01em; max-width:60ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; transition:color .2s ease;}
.nowTitle{font-family:var(--brand); font-weight:600; font-size:15px; letter-spacing:-.02em; line-height:1.2; max-width:60ch; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; transition:color .2s ease;}
.nowTitle:hover{color:rgba(235,240,252,.98);}
.nowSub{margin-top:4px; color:var(--textMuted); font-size:11px; font-family:var(--mono); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:80ch; transition:color .2s ease;}
.nowSub{margin-top:3px; color:var(--textMuted); font-size:11px; font-family:var(--mono); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:80ch; transition:color .2s ease;}
.dividerWrap{display:flex; align-items:stretch; justify-content:center;}
.divider{width:10px; cursor:col-resize; position:relative; background:transparent; border:none;}
@@ -25,11 +25,11 @@
.dock{flex:1 1 auto; min-height:0; border-top:1px solid rgba(140,160,210,.04); display:grid; grid-template-columns:62% 10px 38%; background:rgba(0,0,0,.06); overflow:hidden;}
.dockPane{min-height:0; display:flex; flex-direction:column; overflow:hidden;}
.dockInner{padding:10px; min-height:0; display:flex; flex-direction:column; gap:8px; height:100%;}
.dockHeader{padding:10px 12px 9px; border:none; border-radius:var(--r2); display:flex; align-items:center; justify-content:space-between; gap:8px; background:var(--surface-2); flex:0 0 auto; transition:background .2s ease;}
.dockHeader{padding:10px 12px 9px; border:none; border-radius:var(--r2); display:flex; align-items:center; justify-content:space-between; gap:8px; background:var(--surface-2); flex:0 0 auto; min-height:47px; transition:background .2s ease;}
.dockHeader:hover{background:var(--surface-3);}
#notesHeader{border-bottom-right-radius:7px;}
#infoHeader{border-bottom-left-radius:7px; margin-right:12px;}
.dockTitle{font-family:var(--brand); font-weight:700; letter-spacing:0; font-size:13px; color:rgba(218,225,240,.92); display:flex; align-items:center; gap:10px; transition:color .2s ease;}
.dockTitle{font-family:var(--brand); font-weight:600; letter-spacing:-.02em; font-size:15px; line-height:1.2; color:rgba(218,225,240,.92); display:flex; align-items:center; gap:10px; transition:color .2s ease;}
.dockHeader:hover .dockTitle{color:rgba(235,240,252,.98);}
.dockTitle .fa{color:var(--iconStrong)!important; opacity:.88; font-size:14px; transition:transform .3s var(--ease-bounce), opacity .2s ease;}
.dockHeader:hover .dockTitle .fa{transform:scale(1.12) rotate(-5deg); opacity:1;}
@@ -37,6 +37,7 @@
.notesArea{flex:1 1 auto; min-height:0; overflow:hidden; position:relative;}
.notesSaved{
position:absolute;
z-index:2;
bottom:12px; right:12px;
padding:6px 10px;
border-radius:var(--r2);
@@ -63,9 +64,20 @@
60%{transform:scale(1.3) rotate(5deg);}
100%{transform:scale(1) rotate(0);}
}
.notes{width:100%; height:100%; resize:none; border-radius:var(--r3); border:1px solid transparent; background:var(--surface-0); color:rgba(218,225,240,.90); padding:10px 12px; outline:none; font-family:var(--sans); font-size:13px; line-height:1.45; letter-spacing:0; box-shadow:var(--shadowInset); overflow:auto; scrollbar-width:thin; scrollbar-color:rgba(140,160,210,.10) transparent; transition:border-color .25s ease, box-shadow .25s ease, background .2s ease;}
.notes:hover{background:rgba(140,165,220,.03);}
.notes:focus{border-color:rgba(136,164,196,.25); box-shadow:var(--shadowInset), 0 0 0 2px rgba(136,164,196,.12);}
/* Notes timestamp highlighting backdrop */
.notesBackdrop{position:absolute; inset:0; padding:10px 12px; border:1px solid transparent; border-radius:var(--r3); background:var(--surface-0); box-shadow:var(--shadowInset); font-family:var(--sans); font-size:13px; line-height:1.45; letter-spacing:0; color:rgba(218,225,240,.90); white-space:pre-wrap; word-break:break-word; overflow-wrap:anywhere; overflow-y:scroll; scrollbar-width:thin; scrollbar-color:transparent transparent; pointer-events:none; transition:border-color .25s ease, box-shadow .25s ease, background .2s ease;}
.notesBackdrop::-webkit-scrollbar{width:2px; height:2px;}
.notesBackdrop::-webkit-scrollbar-track{background:transparent;}
.notesBackdrop::-webkit-scrollbar-thumb{background:transparent;}
.notesBackdrop::-webkit-scrollbar-button{width:0; height:0; display:none;}
.notesBackdrop.hover{background:rgba(140,165,220,.03);}
.notesBackdrop.focus{border-color:rgba(136,164,196,.25); box-shadow:var(--shadowInset), 0 0 0 2px rgba(136,164,196,.12);}
.tsLink{color:rgba(136,164,196,.85); pointer-events:auto; cursor:pointer; transition:color .15s ease;}
.notesBackdrop.hover .tsLink,.notesBackdrop.focus .tsLink{color:rgba(136,164,196,.95);}
.notes{width:100%; height:100%; resize:none; border-radius:var(--r3); border:1px solid transparent; background:transparent; color:transparent; caret-color:rgba(218,225,240,.90); padding:10px 12px; outline:none; font-family:var(--sans); font-size:13px; line-height:1.45; letter-spacing:0; box-shadow:none; overflow:auto; scrollbar-width:thin; scrollbar-color:rgba(140,160,210,.10) transparent; position:relative; z-index:1;}
.notes:hover{background:transparent;}
.notes:focus{border-color:transparent; box-shadow:none;}
.notes::selection{background:rgba(136,164,196,.30);}
.notes::-webkit-scrollbar{width:2px; height:2px;}
.notes::-webkit-scrollbar-track{background:transparent;}
.notes::-webkit-scrollbar-thumb{background:rgba(140,160,210,.10); border-radius:0;}
@@ -127,11 +139,11 @@ dl.kv dt,dl.kv dd{margin:0; padding:0;}
white-space:nowrap;
transition:color .2s ease;
}
.kv:hover .k{color:rgba(148,162,192,.50);}
.kv:hover .k{color:var(--textDim);}
.v{
font-family:var(--brand);
font-size:13px;
font-weight:600;
font-weight:500;
color:var(--text);
letter-spacing:-.01em;
word-break:break-word;
@@ -154,7 +166,7 @@ dl.kv dt,dl.kv dd{margin:0; padding:0;}
margin:-2px 0;
transition:background .2s ease, color .2s ease;
}
.kv:hover .v.mono{background:rgba(136,164,196,.09); color:rgba(170,185,215,.70);}
.kv:hover .v.mono{background:rgba(136,164,196,.09); color:var(--textMuted);}
.dockDividerWrap{display:flex; align-items:stretch; justify-content:center;}
.dockDivider{width:10px; cursor:col-resize; position:relative; background:transparent; border:none;}

View File

@@ -72,7 +72,7 @@ video::cue{
color:rgba(255,255,255,1)!important;
}
.controls{display:flex; flex-direction:column; gap:10px; padding:12px; border-top:1px solid rgba(140,160,210,.04); flex:0 0 auto;}
.controls{display:flex; flex-direction:column; gap:10px; padding:12px; border-top:1px solid rgba(140,160,210,.04); flex:0 0 auto; position:relative; z-index:10;}
.controlsStrip{
display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap;
padding:5px 6px;
@@ -151,7 +151,7 @@ video::cue{
.seek::-moz-range-track{background:transparent; height:18px;}
.seek::-moz-range-thumb{width:16px; height:16px; border-radius:999px; border:2px solid rgba(200,210,230,.28); background:rgba(220,228,240,.88); box-shadow:0 2px 6px rgba(0,0,0,.30); cursor:pointer;}
.miniCtl{display:flex; align-items:center; gap:8px; padding:6px 9px; border-radius:var(--r2); border:none; background:var(--surface-2); box-shadow:var(--shadow3); position:relative; transition:all .2s var(--ease-bounce);}
.miniCtl{display:flex; align-items:center; gap:8px; padding:6px 9px; border-radius:var(--r2); border:none; background:var(--surface-2); box-shadow:var(--shadow3); position:relative; z-index:3; transition:all .2s var(--ease-bounce);}
.miniCtl:hover{background:var(--surface-3); transform:translateY(-1px);}
.miniCtl:active{transform:scale(.97) translateY(0); transition-duration:.08s;}
.miniCtl .fa{font-size:14px; color:var(--iconStrong)!important; opacity:.95; flex:0 0 auto; transition:transform .2s var(--ease-bounce), opacity .15s ease;}
@@ -234,7 +234,7 @@ video::cue{
.progressPill{flex:0 0 auto; display:flex; align-items:center; gap:8px; padding:6px 10px; border-radius:var(--r2); border:none; background:var(--surface-2); box-shadow:var(--shadow3); min-width:220px; transition:background .2s ease;}
.progressPill:hover{background:var(--surface-3);}
.progressLabel{font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--textMuted); margin-right:2px; transition:color .2s ease;}
.progressPill:hover .progressLabel{color:rgba(170,185,215,.70);}
.progressPill:hover .progressLabel{color:var(--textMuted);}
.progressBar{width:120px; height:8px; border-radius:999px; border:none; background:rgba(140,165,220,.04); overflow:hidden; transition:height .2s var(--ease-bounce);}
.progressPill:hover .progressBar{height:10px;}
.progressBar>div{height:100%; width:0%; background:rgba(136,164,196,.75); transition:width .4s var(--ease-spring);}

View File

@@ -75,10 +75,10 @@
.treeSvg circle{fill:rgba(200,212,238,.60); stroke:rgba(136,164,196,.18); stroke-width:1;}
.textWrap{min-width:0; flex:1 1 auto;}
.name{font-size:13px; font-weight:700; letter-spacing:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; transition:color .2s var(--ease-spring), text-shadow .2s ease, transform .2s var(--ease-bounce); transform:translateX(0);}
.name{font-size:13px; font-weight:600; letter-spacing:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; transition:color .2s var(--ease-spring), text-shadow .2s ease, transform .2s var(--ease-bounce); transform:translateX(0);}
.row:hover .name{color:rgba(235,240,252,.96); text-shadow:0 0 20px rgba(136,164,196,.08); transform:translateX(4px);}
.small{margin-top:4px; font-size:11px; color:var(--textMuted); font-family:var(--mono); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; transition:color .2s ease;}
.row:hover .small{color:rgba(170,182,210,.75);}
.row:hover .small{color:var(--textMuted);}
.tag{flex:0 0 auto; display:inline-flex; align-items:center; padding:5px 9px; border-radius:var(--r2); border:none; background:var(--surface-2); color:var(--textMuted); font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; margin-top:2px; transition:all .2s var(--ease-bounce);}
.row:hover .tag{transform:scale(1.05);}
@@ -105,7 +105,7 @@
.empty{padding:10px 12px; color:var(--textMuted); font-size:13px; line-height:1.4;}
.playlistHeader{font-family:var(--brand); font-weight:800; letter-spacing:-.01em; font-size:14px; cursor:help; display:flex; align-items:center; gap:10px; transition:color .2s ease;}
.playlistHeader{font-family:var(--brand); font-weight:600; letter-spacing:-.02em; font-size:15px; line-height:1.2; cursor:help; display:flex; align-items:center; gap:10px; transition:color .2s ease;}
.playlistHeader:hover{color:rgba(235,240,252,.98);}
.playlistHeader .fa{color:var(--iconStrong)!important; opacity:.92; transition:transform .3s var(--ease-bounce), opacity .2s ease;}
.playlistHeader:hover .fa{transform:rotate(-8deg) scale(1.12); opacity: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;