@braccato/core 0.1.6 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/README.md ADDED
@@ -0,0 +1,298 @@
1
+ # @braccato/core
2
+
3
+ A custom element that renders synchronized lyrics and lights each syllable up as it is sung. No
4
+ runtime dependencies, only a types-only one on `@braccato/types`. The lines go into light DOM rather
5
+ than a shadow root, so the CSS already on your page reaches them.
6
+
7
+ Extracted from the [Better Lyrics](https://better-lyrics.boidu.dev) rendering engine, which is still
8
+ where it runs.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm i @braccato/core
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```html
19
+ <audio id="player" src="song.mp3" controls></audio>
20
+ <braccato-lyrics source="#player"></braccato-lyrics>
21
+
22
+ <script type="module">
23
+ import "@braccato/core/element";
24
+ import "@braccato/core/styles/variables.css";
25
+ import "@braccato/core/styles/lyrics.css";
26
+ import "@braccato/core/styles/instrumental.css";
27
+
28
+ document.querySelector("braccato-lyrics").lyrics = [
29
+ { startTimeMs: 0, durationMs: 4200, words: "The first line" },
30
+ { startTimeMs: 4200, durationMs: 3800, words: "The second" },
31
+ ];
32
+ </script>
33
+ ```
34
+
35
+ `source` takes a CSS selector or a media element. It is resolved when the element connects, so put
36
+ the `<audio>` before the tag, or write the property from script. Without a source, drive the view
37
+ yourself by writing `currentTime` and `playing`.
38
+
39
+ Two things catch everybody once. The element has no `display` of its own:
40
+
41
+ ```css
42
+ braccato-lyrics {
43
+ display: block;
44
+ }
45
+ ```
46
+
47
+ And autoscroll writes `scrollTop` on the nearest ancestor that scrolls, falling through to the
48
+ document when nothing does. If the element is not inside its own scroller, say which one it is:
49
+
50
+ ```js
51
+ view.host = { getScrollElement: () => yourFrame };
52
+ ```
53
+
54
+ ## Lyrics
55
+
56
+ The array is the whole input, and nothing in this package produces one.
57
+ [`@braccato/parsers`](https://www.npmjs.com/package/@braccato/parsers) reads TTML, LRC, SRT, QRC and
58
+ plain text, and picks between them by looking at the file.
59
+
60
+ ```js
61
+ import { detectParser } from "@braccato/parsers";
62
+
63
+ const text = await fetch("song.ttml").then(response => response.text());
64
+ view.lyrics = detectParser(text).parse(text, player.duration * 1000);
65
+ ```
66
+
67
+ A `Lyric` is `{ startTimeMs, durationMs, words }`, with an optional `parts` array of the same three
68
+ fields for syllable or word timing, and optional `translation`, `romanization` and
69
+ `timedRomanization` beside them.
70
+
71
+ ## Properties
72
+
73
+ Every one of these may be written before the element is in a document. The renderer is built when it
74
+ connects, and everything it was handed by then is applied at once.
75
+
76
+ | Property | Attribute | Type | Default | Description |
77
+ | --------------- | -------------- | ----------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
78
+ | `lyrics` | | `Lyric[] \| null` | `null` | The song. Null means it was never given one, and an empty array clears the view, so there is a way to say both. |
79
+ | `lyricsOptions` | | `{ loaderVisible?, noLyrics? }` | `{}` | How the lines are built. `noLyrics` marks a message as a placeholder rather than a song, which keeps passive scrolling from drifting it. |
80
+ | `source` | `source` | `string \| HTMLMediaElement \| null` | `null` | A selector or the media element itself. See Following a media element. |
81
+ | `mediaElement` | | `HTMLMediaElement \| null` (get) | `null` | What `source` resolved to. Null while disconnected, and null for a selector that missed. |
82
+ | `currentTime` | `current-time` | `number` | `0` | Playback position in **seconds**. Writing it renders the view again, so whoever holds the clock drives the lyrics by writing this. |
83
+ | `playing` | `playing` | `boolean` | `false` | A paused view animates differently from a playing one. |
84
+ | `tickOptions` | | `ElementTickOptions` | `{}` | The rest of a tick: four offsets taken off the clock before it is matched, whether passive scrolling is on, and when the clock was sampled. |
85
+ | `theme` | `theme` | `string` | `""` | A compiled stylesheet. See Theming. |
86
+ | `host` | | `Partial<LyricsRendererHost>` | `{}` | Overrides for what the renderer asks of its surroundings. Every member has a default. Writing it while connected rebuilds the view. |
87
+ | `renderer` | | `LyricsRenderer \| null` (get) | `null` | The renderer underneath, for the day the tag runs out. A different one after every reconnection. |
88
+ | `status` | | `ElementStatus` (get) | `"idle"` | `idle`, `rendering`, `theme-conflict` or `no-browsing-context`. |
89
+
90
+ `tickOptions` and `lyricsOptions` are stored on write and read by the next tick or the next build, so
91
+ writing options and the clock on the same frame renders once.
92
+
93
+ ## Attributes
94
+
95
+ An attribute writes its property, and a property never writes back. Reflecting `current-time` would
96
+ put the playback clock into the DOM sixty times a second, and one attribute reflecting while the rest
97
+ do not is worse than none of them doing it.
98
+
99
+ | Attribute | Writes | Notes |
100
+ | -------------- | ------------- | -------------------------------------------------------------------------------------------------- |
101
+ | `source` | `source` | The selector form only. Another selector moves the binding, and removing it unbinds. |
102
+ | `theme` | `theme` | A whole stylesheet in an attribute value. It works, but nobody would ship a theme this way. |
103
+ | `current-time` | `currentTime` | Seconds. A value that does not parse as a number is ignored rather than read as zero. |
104
+ | `playing` | `playing` | An ordinary boolean attribute: its presence is what counts, so `playing="false"` is playing. |
105
+
106
+ ## Events
107
+
108
+ All four bubble and are composed, so an element you put inside your own shadow root still reaches
109
+ your listener.
110
+
111
+ | Event | Detail | When |
112
+ | ------------------------ | ------------------------- | --------------------------------------------------------------------------------- |
113
+ | `braccato:lyrics-loaded` | `{ lineCount, syncType }` | Lyrics were applied, including an empty array. A theme change that rebuilds the lines reports itself the same way. |
114
+ | `braccato:line-click` | `{ timeS }` | A line was clicked. The seek has already reached the bound media element by the time you hear about it. |
115
+ | `braccato:scroll-state` | `{ userScrolling }` | Autoscroll stopped following the song, or started again. |
116
+ | `braccato:error` | `{ phase, error }` | Connecting, resolving a source, or applying lyrics or a theme went wrong. `phase` is `connect`, `conflict`, `source`, `lyrics` or `theme`. |
117
+
118
+ Errors are dispatched a microtask after they happen rather than where they happen, which is what
119
+ makes them receivable at all: `connectedCallback` runs before any listener a page could have added.
120
+ A listener added later than that still misses them, so `status` answers the same question and needs
121
+ no listener. Nothing thrown by a tick lands here, because sixty error events a second would bury the
122
+ one that mattered.
123
+
124
+ There is no `braccato:word-click`. The renderer tells its host `seek(timeS)` and nothing else, so the
125
+ element cannot tell a word seek from a line seek without re-deriving the click branch off the DOM.
126
+ The DOM is light and the class names are published, so listen for `click` on the element and read
127
+ `.blyrics--word` yourself.
128
+
129
+ ## Theming
130
+
131
+ A theme is a stylesheet. Write CSS against the class names below and the module stays out of it. What
132
+ it does read is the `blyrics-*` lines inside the comments, which is how a theme changes behaviour
133
+ without a second configuration format.
134
+
135
+ ```js
136
+ view.theme = `
137
+ /* blyrics-target-scroll-pos-ratio = 0.5; */
138
+ /* blyrics-long-word-threshold = 900; */
139
+
140
+ .blyrics-container {
141
+ --blyrics-font-size: 3.5rem;
142
+ --blyrics-lyric-active-color: white;
143
+ --blyrics-lyric-inactive-color: rgb(255 255 255 / 0.25);
144
+ }
145
+ `;
146
+ ```
147
+
148
+ Settings are read from comments only. Everything else is CSS the browser is going to read, and a
149
+ stylesheet must not be able to configure the module by accident. An empty theme puts every setting
150
+ back to its default. The stylesheet itself goes into the document head under the
151
+ `blyrics-custom-style` id.
152
+
153
+ There is no `longWordThreshold`, `lineSyncedDelay` or `disableRichsync` property. Those are theme
154
+ settings (`blyrics-long-word-threshold`, `blyrics-line-synced-animation-delay`,
155
+ `blyrics-disable-richsync`), read from the stylesheet you already hand over. A theme that set one
156
+ while a property said otherwise would leave the module with two answers and no rule for picking.
157
+
158
+ `parseThemeConfig` is published on `@braccato/core/themeSettings` for reading the settings out of a
159
+ stylesheet somewhere no renderer is running.
160
+
161
+ ### Custom properties
162
+
163
+ The ones a theme reaches for first. `variables.css` declares the rest.
164
+
165
+ ```css
166
+ .blyrics-container {
167
+ --blyrics-font-family: system-ui, sans-serif;
168
+ --blyrics-font-size: 3rem;
169
+ --blyrics-line-height: 1.333;
170
+ --blyrics-padding: 2rem;
171
+ --blyrics-lyric-active-color: white;
172
+ --blyrics-lyric-inactive-color: rgb(255 255 255 / 0.3);
173
+ --blyrics-glow-color: rgb(255 255 255 / 0.5);
174
+ }
175
+ ```
176
+
177
+ `--blyrics-font-size` is what everything else is sized off, including the instrumental dots.
178
+ `--blyrics-padding` is the vertical room around each line, and the one to reach for before
179
+ `line-height`. Every word is given the glow, so a theme that wants it to mean something selects on
180
+ `data-long-word`, which the module sets on any part held past `blyrics-long-word-threshold`.
181
+
182
+ ### Class names
183
+
184
+ These are published API rather than implementation. Renaming one costs a migration rather than a
185
+ refactor. Import them from `@braccato/core/constants` instead of typing them out.
186
+
187
+ | Constant | Class | What it is |
188
+ | ------------------------- | --------------------------- | ----------------------------------------------------------- |
189
+ | `LYRICS_CLASS` | `blyrics-container` | The view. One per renderer. |
190
+ | `LINE_CLASS` | `blyrics--line` | One line, carrying its own `dir="auto"`. |
191
+ | `CURRENT_LYRICS_CLASS` | `blyrics--active` | The line the song is on right now. |
192
+ | `WORD_CLASS` | `blyrics--word` | One word, and the unit the sweep animates. |
193
+ | `BACKGROUND_LYRIC_CLASS` | `blyrics-background-lyric` | A background vocal, sung over the line it answers. |
194
+ | `USER_SCROLLING_CLASS` | `blyrics-user-scrolling` | Set while a reader has scrolled away and autoscroll waits. |
195
+ | `TRANSLATED_LYRICS_CLASS` | `blyrics--translated` | A translation hung off a line that was already built. |
196
+ | `CUSTOM_THEME_STYLE_ID` | `blyrics-custom-style` | The id of the `<style>` the theme lands in. |
197
+
198
+ ## Stylesheets
199
+
200
+ Three sheets ship with the package, and loading them is yours, the way any package's CSS is. Leave
201
+ them out and you get lines that are in the document and unstyled, rather than lines that are missing.
202
+
203
+ | File | What it carries |
204
+ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
205
+ | `@braccato/core/styles/variables.css` | Every `--blyrics-*` default. It goes first, because the other two read from it. |
206
+ | `@braccato/core/styles/lyrics.css` | The container, the lines, the words and the sweep, plus two `@property` registrations the word animation interpolates through. |
207
+ | `@braccato/core/styles/instrumental.css` | The waveform that fills a bar nobody sings over, and the animation that walks it. |
208
+
209
+ One thing they do not do for you. The module measures the room the first and last lines need to reach
210
+ the view's target scroll position and writes it on the root as `--blyrics-padding-top` and
211
+ `--blyrics-padding-bottom`, but `lyrics.css` only spends the bottom one. Supply the top rule:
212
+
213
+ ```css
214
+ .blyrics-container {
215
+ padding-top: var(--blyrics-padding-top, 2rem);
216
+ }
217
+ ```
218
+
219
+ ## Light DOM, not shadow DOM
220
+
221
+ The element builds into itself. That is what lets a stylesheet at document level select the lines,
222
+ and what lets the package's own `@property` registrations apply to them, which they would not inside
223
+ a shadow root. The theme is adopted into the element's document rather than encapsulated, and the
224
+ package's stylesheets are yours to load for the same reason.
225
+
226
+ ## Entry points
227
+
228
+ `@braccato/core` is the facade and registers nothing. `createLyricsRenderer(options)` returns one
229
+ `LyricsRenderer`: give it lyrics, tick it, and it owns the DOM it builds and every re-measurement
230
+ that DOM needs. `resetPlaybackClock`, `resumeAllAutoscroll`, `injectRomanization` and
231
+ `injectTranslation` are published beside it, for what one instance cannot answer for on its own.
232
+
233
+ `@braccato/core/element` registers `<braccato-lyrics>`, and `<better-lyrics>` beside it, on import.
234
+ Registration is a side effect, which is why it is entered separately.
235
+
236
+ Four leaves import nothing at all, so taking one does not pull the engine into your bundle with it:
237
+
238
+ - `@braccato/core/constants` for the class names and element ids above
239
+ - `@braccato/core/text` for script detection: `testRtl`, `containsNonLatin`, `detectNonLatinLanguage`
240
+ - `@braccato/core/themeSettings` for `parseThemeConfig`
241
+ - `@braccato/core/util` for pure helpers such as `clamp` and `toMs`
242
+
243
+ Two notes on the element entry point. A browser extension's isolated world has no custom element
244
+ registry, so `window.customElements` is null there and importing this file throws where it registers.
245
+ An extension that wants the tag has to run in the page's own world; one that stays isolated calls
246
+ `createLyricsRenderer` directly. And registration is silent about a name already taken, so two copies
247
+ of this package on one page means the first to load takes both names and `instanceof` against the
248
+ second copy's class is false for every element on the page. Load one copy.
249
+
250
+ ## Following a media element
251
+
252
+ While a `source` is bound, the element drives itself. It reads `currentTime` and `paused` off the
253
+ media element on a `requestAnimationFrame` loop that runs only while the song plays, and a click on a
254
+ lyric line sets `currentTime` back on it. So `currentTime` and `playing` become outputs: a write to
255
+ either is dropped and the getter keeps reporting what the binding last read. Dropped rather than
256
+ reported, because a consumer who bound a source and left their own frame loop running would otherwise
257
+ be told about it sixty times a second. Unbind and the clock goes back to whoever asked for it.
258
+
259
+ A reading the media element has not refreshed yet is carried forward at the playback rate it was
260
+ taken at, capped at 100ms of frame time. That cap is what covers a stall: the view runs at most 100ms
261
+ past the last real reading and then waits with it. What it costs is a step backwards when the clock
262
+ moves again, scaled by the rate. 100ms at 1x, 400ms at 4x.
263
+
264
+ `play`, `pause`, `seeking`, `seeked` and `ratechange` are listened to. The frame loop covers the rest
265
+ by asking the media element whether its clock is still going rather than trusting that something said
266
+ so. One gap is worth knowing: `emptied` while already paused leaves no loop running to notice, so
267
+ swapping `audio.src` between songs without playing goes on reporting the old position until the next
268
+ `play`.
269
+
270
+ ## One renderer per document
271
+
272
+ Two renderers in one document write over each other, so the module supports one. It is a constraint
273
+ rather than a setting, and it is stated rather than enforced: none of the points where two of them
274
+ collide is a crash.
275
+
276
+ Two things are written per document and belong to whichever renderer wrote them last: the theme's
277
+ `<style>` element, and the scroll padding on the root. Two more are per bundle, because a settings
278
+ registry and the playback clock both live at module scope: one theme means one set of values for
279
+ every view in that bundle, and whichever view ticked last is the one whose clock the others replay.
280
+
281
+ None of that is a limit on how many elements you may have. Two views handed the **same** theme share
282
+ only the settings both of them asked for, and the renderer adopts an existing theme element rather
283
+ than adding a rival under the same id. The line is drawn at the disagreement: when an element applies
284
+ a theme another element in its document was not given, both dispatch `braccato:error` with
285
+ `phase: "conflict"` and both read `status === "theme-conflict"`. Neither stops rendering, because a
286
+ blank view with a reason is worse than a themed one with a warning.
287
+
288
+ ## Docs and demo
289
+
290
+ Full documentation is at [braccato.boidu.dev](https://braccato.boidu.dev).
291
+
292
+ The demo page lives at [`demo/`](https://github.com/better-lyrics/braccato/tree/master/demo) in the
293
+ repository and runs against the emitted package, with a control for most of what is above. Clone the
294
+ repository and run `pnpm -C demo dev`, then open `http://localhost:5173/`.
295
+
296
+ ## Licence
297
+
298
+ MIT. See `LICENSE`.
@@ -0,0 +1,25 @@
1
+ export declare const LYRICS_WRAPPER_ID: "blyrics-wrapper";
2
+ export declare const LYRICS_CLASS: "blyrics-container";
3
+ export declare const LINE_CLASS: "blyrics--line";
4
+ export declare const WORD_CLASS: "blyrics--word";
5
+ export declare const FOOTER_CLASS: "blyrics-footer";
6
+ export declare const CURRENT_LYRICS_CLASS: "blyrics--active";
7
+ export declare const ANIMATING_CLASS: "blyrics--animating";
8
+ export declare const PAUSED_CLASS: "blyrics--paused";
9
+ export declare const ZERO_DURATION_ANIMATION_CLASS: "blyrics-zero-dur-animate";
10
+ export declare const USER_SCROLLING_CLASS: "blyrics-user-scrolling";
11
+ export declare const BACKGROUND_LYRIC_CLASS: "blyrics-background-lyric";
12
+ export declare const EXPLICIT_WORD_CLASS: "blyrics-explicit";
13
+ export declare const RTL_CLASS: "blyrics-rtl";
14
+ export declare const TRANSLATED_LYRICS_CLASS: "blyrics--translated";
15
+ export declare const ROMANIZED_LYRICS_CLASS: "blyrics--romanized";
16
+ export declare const CONTENT_LINE_CLASS: "blyrics-content-line";
17
+ export declare const LINE_MAIN_CLASS: "blyrics-line-main";
18
+ export declare const BACKGROUND_LINE_CLASS: "blyrics-background-line";
19
+ export declare const WORD_GROUP_CLASS: "blyrics-word-group";
20
+ export declare const LONG_WORD_GROUP_CLASS: "blyrics-word-group-long";
21
+ export declare const WORD_HIGHLIGHT_CLASS: "blyrics-word-highlight";
22
+ export declare const LINE_SYNCED_WORD_CLASS: "blyrics-line-synced-word";
23
+ export declare const BIDI_RUN_CLASS: "blyrics-bidi-run";
24
+ export declare const BIDI_SENSITIVE_CLASS: "blyrics-bidi-sensitive";
25
+ export declare const CUSTOM_THEME_STYLE_ID: "blyrics-custom-style";
@@ -0,0 +1,42 @@
1
+ // These names are a public contract: marketplace themes select on them and set properties through
2
+ // them, so renaming one is a breaking change for every published theme, not a refactor.
3
+ // -- Structure --------------------------------------------
4
+ export const LYRICS_WRAPPER_ID = "blyrics-wrapper";
5
+ export const LYRICS_CLASS = "blyrics-container";
6
+ export const LINE_CLASS = "blyrics--line";
7
+ export const WORD_CLASS = "blyrics--word";
8
+ export const FOOTER_CLASS = "blyrics-footer";
9
+ // -- Playback state --------------------------------------------
10
+ export const CURRENT_LYRICS_CLASS = "blyrics--active";
11
+ export const ANIMATING_CLASS = "blyrics--animating";
12
+ export const PAUSED_CLASS = "blyrics--paused";
13
+ export const ZERO_DURATION_ANIMATION_CLASS = "blyrics-zero-dur-animate";
14
+ export const USER_SCROLLING_CLASS = "blyrics-user-scrolling";
15
+ // -- Line and word variants --------------------------------------------
16
+ export const BACKGROUND_LYRIC_CLASS = "blyrics-background-lyric";
17
+ export const EXPLICIT_WORD_CLASS = "blyrics-explicit";
18
+ export const RTL_CLASS = "blyrics-rtl";
19
+ export const TRANSLATED_LYRICS_CLASS = "blyrics--translated";
20
+ export const ROMANIZED_LYRICS_CLASS = "blyrics--romanized";
21
+ // -- Line internals --------------------------------------------
22
+ export const CONTENT_LINE_CLASS = "blyrics-content-line";
23
+ export const LINE_MAIN_CLASS = "blyrics-line-main";
24
+ export const BACKGROUND_LINE_CLASS = "blyrics-background-line";
25
+ export const WORD_GROUP_CLASS = "blyrics-word-group";
26
+ export const LONG_WORD_GROUP_CLASS = "blyrics-word-group-long";
27
+ export const WORD_HIGHLIGHT_CLASS = "blyrics-word-highlight";
28
+ export const LINE_SYNCED_WORD_CLASS = "blyrics-line-synced-word";
29
+ export const BIDI_RUN_CLASS = "blyrics-bidi-run";
30
+ export const BIDI_SENSITIVE_CLASS = "blyrics-bidi-sensitive";
31
+ // -- Theme --------------------------------------------
32
+ // The element a theme handed to `setTheme` is applied through, one per document a renderer builds
33
+ // in. Named rather than anonymous because a consumer with a second document to style has to be able
34
+ // to find the first one: this extension's floating window mirrors the side panel's by id.
35
+ //
36
+ // One renderer per document owns it. Two renderers in one document render against one theme
37
+ // whatever they are given, because the settings registry is module scope, so this is a constraint
38
+ // stated rather than a configuration supported. A renderer that finds the id already in its
39
+ // document writes into that element rather than adding a rival, so the id stays unique and a
40
+ // consumer reading it by id gets the sheet in force, and `destroy` takes the element away only if
41
+ // this renderer is what put it there.
42
+ export const CUSTOM_THEME_STYLE_ID = "blyrics-custom-style";
@@ -0,0 +1,155 @@
1
+ import type { Lyric, LyricsRenderer, LyricsRendererHost, LyricSyncType, TickOptions } from "./types.js";
2
+ import type { SetLyricsOptions } from "./view.js";
3
+ /**
4
+ * What a tick carries beyond the clock. `isPlaying` is not among them: `playing` is the property
5
+ * that says so, and one question with two answers has no rule for choosing between them.
6
+ *
7
+ * @public
8
+ */
9
+ export type ElementTickOptions = Omit<TickOptions, "isPlaying">;
10
+ /**
11
+ * How the lines are built, beyond the lines themselves. The mount is not among them: the element is
12
+ * the mount.
13
+ *
14
+ * @public
15
+ */
16
+ export type ElementLyricsOptions = Partial<SetLyricsOptions>;
17
+ /** @public */
18
+ export type ElementErrorPhase = "connect" | "conflict" | "lyrics" | "source" | "theme";
19
+ /** @public */
20
+ export interface ElementErrorDetail {
21
+ /** What the element was doing. Nothing thrown by a tick is reported here: see the README. */
22
+ phase: ElementErrorPhase;
23
+ error: Error;
24
+ }
25
+ /**
26
+ * What the element is doing, and why it is not doing what it was asked. Every `braccato:error` is
27
+ * dispatched a microtask after the fact so that a listener added straight after the element was
28
+ * inserted still hears it; this is the answer for a consumer that added one later than that, or
29
+ * never.
30
+ *
31
+ * @public
32
+ */
33
+ export type ElementStatus = "idle" | "rendering" | "theme-conflict" | "no-browsing-context";
34
+ /** @public */
35
+ export interface LyricsLoadedDetail {
36
+ lineCount: number;
37
+ syncType: LyricSyncType;
38
+ }
39
+ /** @public */
40
+ export interface LineClickDetail {
41
+ /** Where the click asked the player to go, in seconds. */
42
+ timeS: number;
43
+ }
44
+ /** @public */
45
+ export interface ScrollStateDetail {
46
+ /** Whether the user has scrolled away, so autoscroll is waiting rather than following the song. */
47
+ userScrolling: boolean;
48
+ }
49
+ /**
50
+ * Mounts a lyrics view into itself. The renderer is built when the element is connected and
51
+ * destroyed when it is disconnected, so an element that is moved around the page rebuilds rather
52
+ * than going quiet, and every property may be written before either has happened.
53
+ *
54
+ * `dir` is not among the properties, and that is the point: `HTMLElement` already reflects it, the
55
+ * lines this module builds carry `dir="auto"` and resolve their own direction from their text, and a
56
+ * property here would be a second opinion about a question the platform has already answered.
57
+ */
58
+ export declare class BraccatoLyricsElement extends HTMLElement {
59
+ #private;
60
+ static readonly observedAttributes: string[];
61
+ /**
62
+ * The song. An empty array clears the view, so a consumer between songs has a way to say so.
63
+ */
64
+ get lyrics(): Lyric[] | null;
65
+ set lyrics(lyrics: Lyric[] | null);
66
+ /**
67
+ * How the lines are built, beyond the lines themselves: whether a loader is still covering the
68
+ * view, and whether these lyrics are a "not found" placeholder rather than a song. The second one
69
+ * is what keeps passive scrolling off a one line message it would otherwise drift for the length
70
+ * of the song.
71
+ *
72
+ * Read by the next build rather than causing one, so a consumer writes it beside `lyrics` rather
73
+ * than instead of it, and writing both renders once.
74
+ */
75
+ get lyricsOptions(): ElementLyricsOptions;
76
+ set lyricsOptions(options: ElementLyricsOptions);
77
+ /**
78
+ * The media element the lyrics follow, as a CSS selector resolved in this element's own document
79
+ * or as the element itself. Setting it binds and null unbinds, and while it is bound the element
80
+ * reads the clock rather than being told it: `currentTime` and `playing` become what it reports.
81
+ *
82
+ * Bound only while connected, the way the renderer is built only while connected, and a selector
83
+ * is resolved again every time it is written and every time the element connects.
84
+ */
85
+ get source(): HTMLMediaElement | string | null;
86
+ set source(source: HTMLMediaElement | string | null);
87
+ /**
88
+ * The media element `source` resolved to. Null whenever nothing is being followed, which is the
89
+ * answer for a selector that matched nothing and for an element that is not connected.
90
+ */
91
+ get mediaElement(): HTMLMediaElement | null;
92
+ /**
93
+ * Playback position in seconds, not milliseconds: the module ticks in seconds, and converting here
94
+ * would leave the element and the renderer underneath it disagreeing about what a number means.
95
+ * Writing it renders the view again, so whoever owns the clock drives the lyrics by writing this.
96
+ *
97
+ * While a media element is bound it is the one that owns the clock, so a write is dropped and this
98
+ * keeps reporting what the binding last read. Dropped rather than reported: a consumer who left
99
+ * their own frame loop running would otherwise be told about it sixty times a second.
100
+ */
101
+ get currentTime(): number;
102
+ set currentTime(currentTimeS: number);
103
+ /** An output rather than an input while a media element is bound, exactly as `currentTime` is. */
104
+ get playing(): boolean;
105
+ set playing(playing: boolean);
106
+ /**
107
+ * The rest of a tick: the user offsets the clock is matched against, whether passive scrolling is
108
+ * switched on for unsynced lyrics, and the timestamp of the player snapshot the clock came from.
109
+ *
110
+ * That last one matters beyond this element. The playback clock the module compares a tick
111
+ * against is module scope, so an element sharing a realm with another view has to be given the
112
+ * same snapshot timestamps that view is, or every tick reads as a jump away from the other one.
113
+ *
114
+ * Read by the next tick rather than causing one, so a consumer that writes these and the clock on
115
+ * the same frame renders the view once rather than twice.
116
+ */
117
+ get tickOptions(): ElementTickOptions;
118
+ set tickOptions(options: ElementTickOptions);
119
+ /**
120
+ * A compiled stylesheet. Its `blyrics-*` comments configure the module and the sheet itself goes
121
+ * into this element's document. An empty one puts every setting back to its default, and is
122
+ * applied like any other: the settings are module scope, so an element that applied nothing would
123
+ * render against whatever the last theme in that bundle left behind. What that costs is that
124
+ * connecting an element nobody gave a theme empties the theme element already in its document.
125
+ */
126
+ get theme(): string;
127
+ set theme(css: string);
128
+ /**
129
+ * Overrides for anything the renderer asks of its surroundings. Every member has a default, so a
130
+ * consumer with nothing to say leaves this alone. Writing it while connected rebuilds the view:
131
+ * the renderer is handed its host once, when it is created.
132
+ */
133
+ get host(): Partial<LyricsRendererHost>;
134
+ set host(overrides: Partial<LyricsRendererHost>);
135
+ /**
136
+ * The renderer underneath, for a consumer who outgrows the element. Null while disconnected, and a
137
+ * different one after every reconnection.
138
+ */
139
+ get renderer(): LyricsRenderer | null;
140
+ /**
141
+ * What the element is doing, asked rather than listened for. `theme-conflict` is the one that says
142
+ * the view is on the screen but not necessarily the way it was asked for: the theme settings are
143
+ * module scope, so a document with two elements holding different themes renders both against
144
+ * whichever was applied last.
145
+ *
146
+ * A `source` that named nothing to follow is deliberately not one of these. The element is still
147
+ * rendering, and a status saying otherwise would trade one true answer for another. What a
148
+ * consumer who was not listening for the error reads instead is `mediaElement`, which is null
149
+ * while `source` still holds the selector it could not resolve.
150
+ */
151
+ get status(): ElementStatus;
152
+ connectedCallback(): void;
153
+ disconnectedCallback(): void;
154
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void;
155
+ }