first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
# `karaoke/lyrics-display`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`<LyricsDisplay>` is the karaoke surface that renders a stack of
lyric lines with the active line highlighted. It is the visible
end-product of the `parseLrc` → `useLyrics` pipeline and the
component the player variants compose when their `lrc` prop is
populated.
The component is clock-agnostic by design: the caller passes
`currentSec` (a plain number) and the component renders the
highlighted line. The typical wiring is `useTransportTime()` from
`@suaveplan/react-audio` when the display sits inside an
`<AudioProvider>`, but standalone preview and test surfaces can
hand in any number — Playwright spec runs use a deterministic
clock; documentation pages drive a `<range>` input. Keeping the time
source explicit removes a hidden provider dependency.
## Features
- Renders one `<span>` per lyric line with `data-active="true|false"`
for CSS-driven styling; consumers needing structural control can
pass `renderLine` for full custom rendering.
- Active line carries `aria-live="polite"` by default so screen
readers announce lyric changes; togglable via `announceLyrics`.
- Wrapping `<div>` exposes `role="region"` + `aria-label="Lyrics"`.
- Re-binds the active index every 100 ms (configurable) regardless
of the consumer's render cadence.
- Both time-source modes share the same internal render path; the
outer dispatch is a one-time component-type pick.
## Usage: Basic
```tsx
import { LyricsDisplay } from "@suaveplan/react-audio-player/karaoke/lyrics-display";
import { useTransportTime } from "@suaveplan/react-audio";
function Player({ lrcSource }: { lrcSource: string }) {
const seconds = useTransportTime();
return <LyricsDisplay lrc={lrcSource} currentSec={seconds} />;
}
```
## Usage: Advanced
```tsx
import { LyricsDisplay } from "@suaveplan/react-audio-player/karaoke/lyrics-display";
import { parseLrc } from "@suaveplan/react-audio-player/karaoke/lrc-parser";
import { useMemo } from "react";
function StyledLyrics({ lrc, time }: { lrc: string; time: number }) {
const lyrics = useMemo(() => parseLrc(lrc), [lrc]);
return (
<LyricsDisplay
lyrics={lyrics}
currentSec={time}
intervalMs={50}
announceLyrics={false}
renderLine={({ lyric, isActive }) => (
<p
style={{
opacity: isActive ? 1 : 0.4,
fontWeight: isActive ? 700 : 400,
}}
>
{lyric.text || <span aria-hidden="true">·</span>}
</p>
)}
/>
);
}
```
## API Reference
- `<LyricsDisplay>` accepts `LyricsDisplayProps`:
- `lyrics?: readonly Lyric[]` — preferred over `lrc`.
- `lrc?: string` — parsed once per identity change.
- `currentSec: number` — required.
- `intervalMs?: number` (default 100).
- `announceLyrics?: boolean` (default `true`).
- `className?: string`.
- `renderLine?: ({ lyric, isActive, index }) => ReactNode`.
## Implementation Notes
The component is a single-branch render — no dual-component
dispatch — because `currentSec` is always a plain prop. This keeps
the Rules of Hooks trivially satisfied (one render path, one hook
sequence) and removes a hidden coupling to the surrounding
`<AudioProvider>`. Consumers that want the transport-driven path
call `useTransportTime()` themselves and pass the result down.
The `data-active="true|false"` attribute is the recommended CSS
selector hook. Toggling `aria-live` only on the active line ensures
exactly one element narrates per change cycle; without that targeted
gating, every line's render would re-announce on every re-mount,
which is acoustically unusable.
When `renderLine` is supplied, the component delegates to it
verbatim and skips the default `<span>` + `data-active` path. The
delegated node is wrapped in a keyed `<Fragment>` so consumers can
return any element they like — `<li>`, `<p>`, `<div>` — without
fighting an enclosing wrapper.