@vestra-player/sdk 1.5.28 → 1.5.30

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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.30
4
+
5
+ **Who must change: nobody.** No API changes, `apiVersion` stays `vestra/1`, no player version needed. Only documentation, its checks and the generated `llms-full.txt` change.
6
+
7
+ - **Every example explains what it does and how it works.** The standard, in *Maintaining* ("Examples in reference comments"): an `@example` has a task title, a lead saying what it does and how it works (checked against the SDK and the player's source), code that compiles on its own with comments on the key lines, and error handling where the user can make a call fail. All 159 examples of the API now meet it (none did before this release), including the 65 of the UI category, the test helpers (`TestHost`, `startPlugin`, `HostError`), the limits and tables (`COMMAND_LIMITS`, `MENU_IDS`, `BUILTIN_*`, `BAR_BUTTON_ICONS`, `CAPABILITY_INFO`, `BINDING_INPUTS`, `OSD_LIMITS`, `SESSION_STORAGE_LIMITS`, `SECRET_LIMITS`, `UI_VALUE_LIMITS`, `THEME_PALETTES`) and the 1.5.29 bar buttons (`CommandButton`, `TestHost.barButtons`). Every example now type checks (61 did not). Examples no longer compose shown sentences in TypeScript: OSD and dialog texts are templates with arguments.
8
+ - **Corrections found on the way.** `Subtitles.setStyle`, `SubtitlesState.style` and `SubtitleStyle`: when several plugins set a style layer, the effective one is the layer of the plugin whose manifest id sorts last, not the latest call. `COMMAND_LIMITS`: `add` never throws `limit`; the player refuses the registration (activation fails, or `Scope.onError`). `startPlugin`'s restart example gives both runs the same `dataDirectory` instead of copying the data before `stop()` (which could miss a pending store write). `TestHost.settle` says the debounced store write is not waited for. `TestHost.dialogAnswers` is one queue for every kind of dialog. `TestHost.openElsewhere`: calls that return nothing (`TimelineLayer.set` with `ifCurrent`) drop the write silently instead of rejecting. Resolver examples (`I18n.error`, `FileResolverOptions`, `TestHost.otherResolvers`) use a made-up `.ch8` format instead of `.nes`, which the store plugin VestraNES owns.
9
+ - **`llms-full.txt` carries the examples and `@remarks`** of every declaration and member (hidden setup lines dropped, `{@includeCode}` inlined), so the plugin workshop's model sees them: 536 KB → 693 KB (about 137k → 177k tokens). `llms.txt` wording trimmed to stay under its 9 KB limit (9,134 bytes).
10
+ - **Hidden setup lines**: in an `@example` code block, lines above `// ---cut---` are type checked but not shown on the site or in `llms-full.txt`.
11
+ - **Checks.** `docs:check` fails on any block that does not compile (it only warned for most examples; `--lenient` keeps the old behaviour for work in progress). `doc-lint` gains `example-lead` (task title, lead, comments, one `// ---cut---` at most; `See` markers for trivial siblings, whose link must resolve, at most 20 % of a declaration's methods) and `stale-refs` (removed or handed-over names such as `vestra.nes`, `.nes` resolvers), enforced for every example and Markdown code block, and prints per category the share of methods with an explained example. `EXAMPLE_BASELINE` keeps known violations per problem when a rule is tightened; it is empty, and `test/doc-baseline.test.ts` keeps it from growing.
12
+ - The parts of `examples/bookmark`, `examples/markdown` and `examples/panel-menu` that the reference includes gained comments on their key lines.
13
+ - Test only: the storage test in `test/round2.test.ts` waits for the debounced file to appear and for `flush()` instead of a fixed 60 ms, and stops its plugins before removing their data directory, so it no longer fails now and then with `ENOENT` on a loaded machine.
14
+
15
+ ## 1.5.29
16
+
17
+ **Who must change: nobody.** Additions only, `apiVersion` stays `vestra/1`. A plugin that does not use `button` is unaffected. A plugin that uses it on a player before 0.5.10 keeps working without a button: the player builds the `plugin` object with its own SDK copy, which does not send the field, so the command and its menu entry register as before (declare `"minPlayerVersion": "0.5.10"` when the button matters). A plugin that mounts its own bottom bar should place `BarButtons` (below); until it does, `vestra-plugin check` warns and the player logs the buttons it hides.
18
+
19
+ - **Commands can have a bottom-bar button** (player 0.5.10, user-approved design of 2026-10-27): `button: { icon, order }` on `commands.add` (`CommandOptions.button`, `CommandButton`, `BarButtonIcon`). The icon is a name of the player's icon set or an image from `plugin.ui.image` (drawn in the icon colour); `order` 0–999 places it left to right. The button needs a menu `path`; its tooltip is the command's title (without a trailing "…") and first key; disabled or busy commands show a disabled button with the reason; a click runs the command like its menu item. One button per plugin, eight in the player (`BAR_BUTTON_LIMITS`; past eight the highest orders stay and the lowest go first, ties: the smaller plugin id, then the earlier declaration); a button past them is left out (the command and its menu entry stay) and the player logs it with `limit`. `add` throws `invalid` for an unknown icon name (`BAR_BUTTON_ICONS`), an order outside 0–999, a released image or a button without a path.
20
+ - **`@vestra/bar-buttons.slint`**: `BarButtons`, the strip of command buttons a bottom bar places at its right end with `room`, the width it can spare. It shows the cells that fit (the player's sidebar toggle, **Toggle Sidebar** `host/sidebar`, rightmost and last to go; cells go from the strip's outer (left) end inward, so of the plugins' buttons the lowest order goes first; `order` is position only) and is as wide as the cells it shows (`first-shown`, out, is the index of the leftmost plugin button shown). The player fills it in every window it composes and runs the clicks. Slint library copy synced; the icon set gains `store` (a shopping bag) and `ai` (a sparkle).
21
+ - **`TestHost.barButtons`**: the plugin's button as the player shows it (`command`, `icon`, `image`, `order`, `enabled`, `checked`, `tooltip`) with `click()`, which runs the command and rejects while it is disabled or once it is gone; empty after the command is disposed. The test host checks buttons as the player does.
22
+ - **`vestra-plugin check` warns** when a plugin mounts a bottom bar whose Slint file does not import `BarButtons` from `@vestra/bar-buttons.slint`, itself or through the plugin's own Slint files it imports (comments do not count; a warning, the check still passes).
23
+ - `BUILTIN_COMMANDS` gains the player's **Toggle Sidebar** (`host/sidebar`, outside the menu, no default key).
24
+ - Docs: *Commands* (concepts) gains *Bottom-bar buttons*; *Commands, menus and bindings* (reference) gains *Bottom-bar buttons*; *Interface guidelines* gains *Writing a bottom bar*; *Limits* gains *Bottom-bar buttons*; *Theme* gains *Bar buttons* and the two icons; *Windows, panels…* points to buttons for a one-click entry point. `llms.txt` and `llms-full.txt` regenerated.
25
+
3
26
  ## 1.5.28
4
27
 
5
28
  **Who must change: nobody.** No API changes, `apiVersion` stays `vestra/1`. Only the SDK's copy of what the player ships changes; a plugin that read `BUILTIN_COMMANDS` or `BUILTIN_SUBMENUS` to avoid a key or a menu path simply finds those entries free now.
@@ -75,6 +75,7 @@ import {
75
75
  basename,
76
76
  dirname,
77
77
  extname,
78
+ isAbsolute,
78
79
  join,
79
80
  relative,
80
81
  resolve,
@@ -356,6 +357,8 @@ async function check(target, report = null) {
356
357
  }
357
358
  say(`${target.name}: capabilities declared for every API use`);
358
359
  if (report) return;
360
+ for (const line of await bottomBarWarnings(target))
361
+ console.log(`${target.name}: warning: ${line}`);
359
362
  // Translation warnings never fail the check.
360
363
  if (present(join(target.directory, "locales"))) {
361
364
  try {
@@ -399,6 +402,106 @@ async function capabilityFindings(target, config) {
399
402
  .map((finding) => ({ ...finding, file: resolve(finding.file) }));
400
403
  }
401
404
 
405
+ /** Slint source without its comments (string literals kept as they are). */
406
+ function slintCode(text) {
407
+ let out = "";
408
+ for (let i = 0; i < text.length;) {
409
+ if (text[i] === '"') {
410
+ let j = i + 1;
411
+ while (j < text.length && text[j] !== '"') j += text[j] === "\\" ? 2 : 1;
412
+ out += text.slice(i, j + 1);
413
+ i = j + 1;
414
+ } else if (text.startsWith("//", i)) {
415
+ const next = text.indexOf("\n", i);
416
+ i = next < 0 ? text.length : next;
417
+ } else if (text.startsWith("/*", i)) {
418
+ const close = text.indexOf("*/", i + 2);
419
+ i = close < 0 ? text.length : close + 2;
420
+ out += " ";
421
+ } else out += text[i++];
422
+ }
423
+ return out;
424
+ }
425
+
426
+ /** Whether `path` lies inside `directory` (not a sibling sharing its prefix). */
427
+ function inside(directory, path) {
428
+ const rel = relative(resolve(directory), path);
429
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
430
+ }
431
+
432
+ /**
433
+ * Whether the Slint file at `file` (relative to the plugin directory)
434
+ * imports `BarButtons` from `@vestra/bar-buttons.slint`, itself or
435
+ * through the plugin's own Slint files (relative paths, any depth, inside
436
+ * the plugin directory). Local re-exports count as such files too:
437
+ * `export { Row } from "row.slint"` and `export * from "row.slint"` are
438
+ * followed, and `export { BarButtons } from "@vestra/bar-buttons.slint"` (or
439
+ * `export * from` it) in a file the bar imports counts as placing it.
440
+ * Comments do not count.
441
+ */
442
+ async function importsBarButtons(target, file, seen = new Set()) {
443
+ const path = resolve(target.directory, file);
444
+ if (seen.has(path) || !inside(target.directory, path)) return false;
445
+ seen.add(path);
446
+ const text = await readFile(path, "utf8").catch(() => null);
447
+ if (text === null) return false;
448
+ const code = slintCode(text);
449
+ // { names (or "*"), from }: imports, and re-exports with a source file.
450
+ const imports = [
451
+ ...code.matchAll(
452
+ /\b(?:import|export)\s*(?:(\*)\s*from\s*|\{([^}]*)\}\s*from\s*)?"([^"]+)"/g,
453
+ ),
454
+ ].map(([, star, names, from]) => ({ names: star ?? names ?? "", from }));
455
+ for (const { names, from } of imports)
456
+ if (
457
+ from === "@vestra/bar-buttons.slint" &&
458
+ (names === "*" ||
459
+ names
460
+ .split(",")
461
+ .some((name) => /^BarButtons(\s+as\s+[\w-]+)?$/.test(name.trim())))
462
+ )
463
+ return true;
464
+ for (const { from } of imports) {
465
+ if (
466
+ from.startsWith("@") ||
467
+ !from.endsWith(".slint") ||
468
+ from === "std-widgets.slint"
469
+ )
470
+ continue;
471
+ if (await importsBarButtons(target, join(dirname(file), from), seen))
472
+ return true;
473
+ }
474
+ return false;
475
+ }
476
+
477
+ /**
478
+ * A plugin that mounts a bottom bar (`panels.mount("bottom", "<file>")`)
479
+ * whose Slint file does not import `BarButtons` (`@vestra/bar-buttons.slint`),
480
+ * itself or through its local imports: while it is the bottom bar, no
481
+ * plugin's command button shows (SDK 1.5.29). The mount is read from the
482
+ * source text: a literal file name in the call.
483
+ */
484
+ async function bottomBarWarnings(target) {
485
+ const { typescript: sources } = await sourceFiles(target);
486
+ const files = new Set();
487
+ for (const file of sources) {
488
+ const text = await readFile(join(target.directory, file), "utf8");
489
+ for (const match of text.matchAll(
490
+ /\.mount\(\s*["'`]bottom["'`]\s*,\s*["'`]([^"'`$]+)["'`]/g,
491
+ ))
492
+ files.add(match[1]);
493
+ }
494
+ const warnings = [];
495
+ for (const file of files) {
496
+ if (!present(join(target.directory, file))) continue;
497
+ if (await importsBarButtons(target, file)) continue;
498
+ warnings.push(
499
+ `${file} is a bottom bar without BarButtons (import { BarButtons } from "@vestra/bar-buttons.slint"): while it is mounted, the command buttons of every plugin (and the player's sidebar toggle) show nowhere; see docs/guidelines/interface.md#writing-a-bottom-bar`,
500
+ );
501
+ }
502
+ return warnings;
503
+ }
504
+
402
505
  /** The plugin's own production .ts / .mts and .slint files. */
403
506
  async function sourceFiles(target) {
404
507
  const all = await files(target.directory);
@@ -49,13 +49,23 @@ export interface CapabilityInfo {
49
49
  * `plugin.i18n`, reading `plugin.window.state`, `plugin.appearance.theme`
50
50
  * and `plugin.packages.list`).
51
51
  *
52
- * @example
52
+ * @example Check in a test that the manifest declares every required capability
53
+ * The test fails, naming the missing capability, when the manifest declares
54
+ * a capability without one its members also need. It reads the plugin's own
55
+ * `manifest.json` and looks each declared name up in `CAPABILITY_INFO`,
56
+ * whose `requires` lists the capabilities that must be declared with it;
57
+ * at run time the player would refuse such a call with code `capability`.
53
58
  * ```ts
59
+ * import assert from "node:assert/strict";
60
+ * import { readFile } from "node:fs/promises";
54
61
  * import { CAPABILITY_INFO } from "@vestra-player/sdk/testing";
55
62
  *
56
- * const manifest = JSON.parse(await readFile("manifest.json", "utf8"));
63
+ * const manifest: { capabilities: string[] } = JSON.parse(
64
+ * await readFile(new URL("../manifest.json", import.meta.url), "utf8"),
65
+ * );
57
66
  * for (const capability of manifest.capabilities)
58
- * for (const needed of CAPABILITY_INFO[capability].requires)
67
+ * // An unknown name has no entry: `vestra-plugin check` reports it.
68
+ * for (const needed of CAPABILITY_INFO[capability]?.requires ?? [])
59
69
  * assert.ok(manifest.capabilities.includes(needed), needed);
60
70
  * ```
61
71
  *
@@ -13,13 +13,23 @@ import { capabilityInfo } from "./capability-rules.js";
13
13
  * `plugin.i18n`, reading `plugin.window.state`, `plugin.appearance.theme`
14
14
  * and `plugin.packages.list`).
15
15
  *
16
- * @example
16
+ * @example Check in a test that the manifest declares every required capability
17
+ * The test fails, naming the missing capability, when the manifest declares
18
+ * a capability without one its members also need. It reads the plugin's own
19
+ * `manifest.json` and looks each declared name up in `CAPABILITY_INFO`,
20
+ * whose `requires` lists the capabilities that must be declared with it;
21
+ * at run time the player would refuse such a call with code `capability`.
17
22
  * ```ts
23
+ * import assert from "node:assert/strict";
24
+ * import { readFile } from "node:fs/promises";
18
25
  * import { CAPABILITY_INFO } from "@vestra-player/sdk/testing";
19
26
  *
20
- * const manifest = JSON.parse(await readFile("manifest.json", "utf8"));
27
+ * const manifest: { capabilities: string[] } = JSON.parse(
28
+ * await readFile(new URL("../manifest.json", import.meta.url), "utf8"),
29
+ * );
21
30
  * for (const capability of manifest.capabilities)
22
- * for (const needed of CAPABILITY_INFO[capability].requires)
31
+ * // An unknown name has no entry: `vestra-plugin check` reports it.
32
+ * for (const needed of CAPABILITY_INFO[capability]?.requires ?? [])
23
33
  * assert.ok(manifest.capabilities.includes(needed), needed);
24
34
  * ```
25
35
  *
@@ -1,21 +1,32 @@
1
1
  /** Finite candidate command registry. Host authenticates owner; no plugin supplies an owner id. */
2
- import type { Transport } from "./wire.ts";
2
+ import type { Json, Transport } from "./wire.ts";
3
3
  import type { ListJson, TextJson } from "./i18n.ts";
4
4
  /**
5
5
  * The player's command registry caps, the same values as the player's own
6
6
  * table (the player's tests check that they agree). They are runaway guards,
7
- * not product limits: a command added past them fails with code `limit`
8
- * (its `ready` rejects; during activation, activation fails), commands
7
+ * not product limits: a command added past them is refused by the player
8
+ * with code `limit` (its `ready` rejects; during activation, activation
9
+ * fails; later the failure goes to `Scope.onError`), commands
9
10
  * already registered are unaffected, and disposing a command frees its slot
10
11
  * at once. {@link TestHost} enforces the same caps.
11
12
  *
12
13
  * For menus built from media content (tracks, chapters, presets), set your
13
14
  * own sensible display cap well below `perPlugin`.
14
15
  *
15
- * @example
16
+ * @example Cap a chapter menu well below the command limit
17
+ * The Chapters submenu lists at most 50 chapters, however many the file
18
+ * has. Every menu item is a command, and the player counts them per plugin.
19
+ * `add` itself returns at once and never throws `limit`: past `perPlugin`
20
+ * the player refuses the registration, which fails the activation or, later,
21
+ * reaches `Scope.onError`. A try/catch around `add` cannot catch that, so a
22
+ * menu built from media content keeps its own cap and leaves room for the
23
+ * plugin's fixed commands.
16
24
  * ```ts
25
+ * declare const chapters: { title: string; time: number }[];
26
+ * // ---cut---
17
27
  * import { COMMAND_LIMITS } from "@vestra-player/sdk/testing";
18
28
  *
29
+ * // 20 slots stay free for the plugin's own commands.
19
30
  * const shown = chapters.slice(0, Math.min(50, COMMAND_LIMITS.perPlugin - 20));
20
31
  * ```
21
32
  */
@@ -36,14 +47,65 @@ export declare const COMMAND_LIMITS: {
36
47
  * `@video` as a menu of its own. The player's tests check that this list
37
48
  * agrees with the player's.
38
49
  *
39
- * @example
50
+ * @example Put a command into the built-in Video menu
51
+ * "Blur" appears in the player's Video menu in every interface language.
52
+ * The player replaces a leading `@video` with that menu's key ("Video")
53
+ * before it merges paths, so the item joins the built-in menu; an id that
54
+ * is not in `MENU_IDS` makes `add` fail with `invalid`, which a test can
55
+ * rule out ahead of time.
40
56
  * ```ts
57
+ * import type { Plugin } from "@vestra-player/sdk";
58
+ * declare const plugin: Plugin;
59
+ * // ---cut---
60
+ * import assert from "node:assert/strict";
41
61
  * import { MENU_IDS } from "@vestra-player/sdk/testing";
42
62
  *
43
- * assert.ok(MENU_IDS.includes("video"));
63
+ * plugin.commands.add({ id: "blur", path: ["@video", "Blur"], run() {} });
64
+ * assert.ok(MENU_IDS.includes("video")); // a menu id, not a title
44
65
  * ```
45
66
  */
46
67
  export declare const MENU_IDS: readonly ["open", "playback", "subtitles", "video", "audio", "screen", "settings"];
68
+ /**
69
+ * The icons a bottom-bar button may name (`CommandOptions.button.icon`):
70
+ * every icon of the player's V-Line set, `@vestra/icons.slint` (draw one
71
+ * in your own Slint with `Icon { name: "store"; }`). The player's tests check
72
+ * that this list is its icon set.
73
+ *
74
+ * @example Check in a test that the button's icon exists
75
+ * The test fails before release if the plugin's bar button names an icon
76
+ * the player does not have. `commands.add` would refuse an unknown name
77
+ * with `invalid`; the player's tests keep `BAR_BUTTON_ICONS` equal to its
78
+ * icon set, so checking against it is the same check without a player.
79
+ * ```ts
80
+ * import assert from "node:assert/strict";
81
+ * import { BAR_BUTTON_ICONS } from "@vestra-player/sdk/testing";
82
+ *
83
+ * assert.ok(BAR_BUTTON_ICONS.includes("store")); // the icon main.ts uses
84
+ * ```
85
+ *
86
+ * @since 1.5.29
87
+ */
88
+ export declare const BAR_BUTTON_ICONS: readonly ["play", "pause", "stop", "back", "forward", "open", "volume", "mute", "playlist", "detach", "attach", "close", "failed", "move-top", "move-up", "move-down", "move-bottom", "search", "chapter", "bookmark", "check-task", "check-box", "menu-check", "submenu", "scroll-up", "scroll-down", "caret-down", "sort-asc", "sort-desc", "tag-remove", "preview-play", "add", "remove", "sort", "fullscreen", "exit-fullscreen", "store", "ai"];
89
+ /**
90
+ * The player's limits on bottom-bar buttons (`CommandOptions.button`), the
91
+ * same values as the player's (its tests check that they agree). A button
92
+ * past `perPlugin` or `total` is left out: its command still registers and
93
+ * its menu entry stays, and the player logs the button it left out
94
+ * (`limit`). Which ones stay: each plugin's first declared button, then the
95
+ * `total` highest by order, plugin id and declaration order: as in a narrow
96
+ * strip, the lowest order goes first (equal orders: the smaller plugin id,
97
+ * then the earlier declaration, goes first).
98
+ *
99
+ * @since 1.5.29
100
+ */
101
+ export declare const BAR_BUTTON_LIMITS: {
102
+ /** Buttons one plugin shows (1). */
103
+ readonly perPlugin: 1;
104
+ /** Buttons the player shows, every plugin together, besides its own sidebar toggle (8). */
105
+ readonly total: 8;
106
+ /** The highest `order` (999; the lowest is 0). */
107
+ readonly maxOrder: 999;
108
+ };
47
109
  /**
48
110
  * A submenu that one of the player's built-in plugins declares: an entry of
49
111
  * {@link BUILTIN_SUBMENUS}.
@@ -71,13 +133,23 @@ export interface BuiltinSubmenu {
71
133
  * listed. The player's tests check that this list agrees with what its
72
134
  * built-in plugins declare.
73
135
  *
74
- * @example
136
+ * @example Add an item to the built-in Snapshot submenu
137
+ * "Copy Snapshot" appears in Video ▸ Snapshot next to the built-in capture
138
+ * plugin's items. Paths merge by their English keys, so a command whose
139
+ * path continues with the submenu's keys (`["Video", "Snapshot"]`, the menu
140
+ * written as `@video`) lands in the same submenu whatever the interface
141
+ * language.
75
142
  * ```ts
143
+ * import type { Plugin } from "@vestra-player/sdk";
144
+ * declare const plugin: Plugin;
145
+ * declare function copySnapshot(): Promise<void>;
146
+ * // ---cut---
76
147
  * import { BUILTIN_SUBMENUS } from "@vestra-player/sdk/testing";
77
148
  *
78
149
  * const snapshot = BUILTIN_SUBMENUS.find((menu) => menu.path.at(-1) === "Snapshot")!;
79
150
  * plugin.commands.add({
80
151
  * id: "snapshot-to-clipboard",
152
+ * // ["@video", "Snapshot", "Copy Snapshot"]; @video stands for "Video".
81
153
  * path: ["@video", ...snapshot.path.slice(1), "Copy Snapshot"],
82
154
  * run: copySnapshot,
83
155
  * });
@@ -120,7 +192,12 @@ export interface BuiltinCommand {
120
192
  * placeholders (`No Audio Tracks`) are. The player's tests check that this
121
193
  * list agrees with what the player and its built-in plugins register.
122
194
  *
123
- * @example
195
+ * @example Check in a test that a default key is still free
196
+ * The test fails if the player or a built-in plugin already binds Ctrl+K
197
+ * globally, before users ever see a conflict. It collects the default
198
+ * bindings of every built-in command that is not limited to its panel's
199
+ * focus (`focus: null`) and looks for the key the plugin wants; bindings
200
+ * are canonical, so compare them as the player writes them.
124
201
  * ```ts
125
202
  * import assert from "node:assert/strict";
126
203
  * import { BUILTIN_COMMANDS } from "@vestra-player/sdk/testing";
@@ -157,6 +234,15 @@ export type CommandDeclaration = {
157
234
  focus: "panel" | null;
158
235
  /** Fire again on keyboard auto-repeat while held. */
159
236
  repeat: boolean;
237
+ /**
238
+ * A bottom-bar button (player 0.5.10): an icon name, or an image in the
239
+ * `plugin.ui.image` wire form. Sent only when set: older players refuse
240
+ * unknown declaration fields.
241
+ */
242
+ button?: {
243
+ icon: string | Json;
244
+ order: number;
245
+ };
160
246
  };
161
247
  export declare function commandClient(transport: Transport): {
162
248
  register: (id: string, revision: number, declaration: CommandDeclaration, state: CommandState) => Promise<{
package/dist/commands.js CHANGED
@@ -1,18 +1,29 @@
1
1
  /**
2
2
  * The player's command registry caps, the same values as the player's own
3
3
  * table (the player's tests check that they agree). They are runaway guards,
4
- * not product limits: a command added past them fails with code `limit`
5
- * (its `ready` rejects; during activation, activation fails), commands
4
+ * not product limits: a command added past them is refused by the player
5
+ * with code `limit` (its `ready` rejects; during activation, activation
6
+ * fails; later the failure goes to `Scope.onError`), commands
6
7
  * already registered are unaffected, and disposing a command frees its slot
7
8
  * at once. {@link TestHost} enforces the same caps.
8
9
  *
9
10
  * For menus built from media content (tracks, chapters, presets), set your
10
11
  * own sensible display cap well below `perPlugin`.
11
12
  *
12
- * @example
13
+ * @example Cap a chapter menu well below the command limit
14
+ * The Chapters submenu lists at most 50 chapters, however many the file
15
+ * has. Every menu item is a command, and the player counts them per plugin.
16
+ * `add` itself returns at once and never throws `limit`: past `perPlugin`
17
+ * the player refuses the registration, which fails the activation or, later,
18
+ * reaches `Scope.onError`. A try/catch around `add` cannot catch that, so a
19
+ * menu built from media content keeps its own cap and leaves room for the
20
+ * plugin's fixed commands.
13
21
  * ```ts
22
+ * declare const chapters: { title: string; time: number }[];
23
+ * // ---cut---
14
24
  * import { COMMAND_LIMITS } from "@vestra-player/sdk/testing";
15
25
  *
26
+ * // 20 slots stay free for the plugin's own commands.
16
27
  * const shown = chapters.slice(0, Math.min(50, COMMAND_LIMITS.perPlugin - 20));
17
28
  * ```
18
29
  */
@@ -33,11 +44,21 @@ export const COMMAND_LIMITS = {
33
44
  * `@video` as a menu of its own. The player's tests check that this list
34
45
  * agrees with the player's.
35
46
  *
36
- * @example
47
+ * @example Put a command into the built-in Video menu
48
+ * "Blur" appears in the player's Video menu in every interface language.
49
+ * The player replaces a leading `@video` with that menu's key ("Video")
50
+ * before it merges paths, so the item joins the built-in menu; an id that
51
+ * is not in `MENU_IDS` makes `add` fail with `invalid`, which a test can
52
+ * rule out ahead of time.
37
53
  * ```ts
54
+ * import type { Plugin } from "@vestra-player/sdk";
55
+ * declare const plugin: Plugin;
56
+ * // ---cut---
57
+ * import assert from "node:assert/strict";
38
58
  * import { MENU_IDS } from "@vestra-player/sdk/testing";
39
59
  *
40
- * assert.ok(MENU_IDS.includes("video"));
60
+ * plugin.commands.add({ id: "blur", path: ["@video", "Blur"], run() {} });
61
+ * assert.ok(MENU_IDS.includes("video")); // a menu id, not a title
41
62
  * ```
42
63
  */
43
64
  export const MENU_IDS = [
@@ -49,6 +70,86 @@ export const MENU_IDS = [
49
70
  "screen",
50
71
  "settings",
51
72
  ];
73
+ /**
74
+ * The icons a bottom-bar button may name (`CommandOptions.button.icon`):
75
+ * every icon of the player's V-Line set, `@vestra/icons.slint` (draw one
76
+ * in your own Slint with `Icon { name: "store"; }`). The player's tests check
77
+ * that this list is its icon set.
78
+ *
79
+ * @example Check in a test that the button's icon exists
80
+ * The test fails before release if the plugin's bar button names an icon
81
+ * the player does not have. `commands.add` would refuse an unknown name
82
+ * with `invalid`; the player's tests keep `BAR_BUTTON_ICONS` equal to its
83
+ * icon set, so checking against it is the same check without a player.
84
+ * ```ts
85
+ * import assert from "node:assert/strict";
86
+ * import { BAR_BUTTON_ICONS } from "@vestra-player/sdk/testing";
87
+ *
88
+ * assert.ok(BAR_BUTTON_ICONS.includes("store")); // the icon main.ts uses
89
+ * ```
90
+ *
91
+ * @since 1.5.29
92
+ */
93
+ export const BAR_BUTTON_ICONS = [
94
+ "play",
95
+ "pause",
96
+ "stop",
97
+ "back",
98
+ "forward",
99
+ "open",
100
+ "volume",
101
+ "mute",
102
+ "playlist",
103
+ "detach",
104
+ "attach",
105
+ "close",
106
+ "failed",
107
+ "move-top",
108
+ "move-up",
109
+ "move-down",
110
+ "move-bottom",
111
+ "search",
112
+ "chapter",
113
+ "bookmark",
114
+ "check-task",
115
+ "check-box",
116
+ "menu-check",
117
+ "submenu",
118
+ "scroll-up",
119
+ "scroll-down",
120
+ "caret-down",
121
+ "sort-asc",
122
+ "sort-desc",
123
+ "tag-remove",
124
+ "preview-play",
125
+ "add",
126
+ "remove",
127
+ "sort",
128
+ "fullscreen",
129
+ "exit-fullscreen",
130
+ "store",
131
+ "ai",
132
+ ];
133
+ /**
134
+ * The player's limits on bottom-bar buttons (`CommandOptions.button`), the
135
+ * same values as the player's (its tests check that they agree). A button
136
+ * past `perPlugin` or `total` is left out: its command still registers and
137
+ * its menu entry stays, and the player logs the button it left out
138
+ * (`limit`). Which ones stay: each plugin's first declared button, then the
139
+ * `total` highest by order, plugin id and declaration order: as in a narrow
140
+ * strip, the lowest order goes first (equal orders: the smaller plugin id,
141
+ * then the earlier declaration, goes first).
142
+ *
143
+ * @since 1.5.29
144
+ */
145
+ export const BAR_BUTTON_LIMITS = {
146
+ /** Buttons one plugin shows (1). */
147
+ perPlugin: 1,
148
+ /** Buttons the player shows, every plugin together, besides its own sidebar toggle (8). */
149
+ total: 8,
150
+ /** The highest `order` (999; the lowest is 0). */
151
+ maxOrder: 999,
152
+ };
52
153
  /**
53
154
  * The submenus the player's built-in plugins declare, by key path, in menu
54
155
  * order (built-in menus as in {@link MENU_IDS}, then top-level submenus).
@@ -59,13 +160,23 @@ export const MENU_IDS = [
59
160
  * listed. The player's tests check that this list agrees with what its
60
161
  * built-in plugins declare.
61
162
  *
62
- * @example
163
+ * @example Add an item to the built-in Snapshot submenu
164
+ * "Copy Snapshot" appears in Video ▸ Snapshot next to the built-in capture
165
+ * plugin's items. Paths merge by their English keys, so a command whose
166
+ * path continues with the submenu's keys (`["Video", "Snapshot"]`, the menu
167
+ * written as `@video`) lands in the same submenu whatever the interface
168
+ * language.
63
169
  * ```ts
170
+ * import type { Plugin } from "@vestra-player/sdk";
171
+ * declare const plugin: Plugin;
172
+ * declare function copySnapshot(): Promise<void>;
173
+ * // ---cut---
64
174
  * import { BUILTIN_SUBMENUS } from "@vestra-player/sdk/testing";
65
175
  *
66
176
  * const snapshot = BUILTIN_SUBMENUS.find((menu) => menu.path.at(-1) === "Snapshot")!;
67
177
  * plugin.commands.add({
68
178
  * id: "snapshot-to-clipboard",
179
+ * // ["@video", "Snapshot", "Copy Snapshot"]; @video stands for "Video".
69
180
  * path: ["@video", ...snapshot.path.slice(1), "Copy Snapshot"],
70
181
  * run: copySnapshot,
71
182
  * });
@@ -413,6 +524,7 @@ vestra.preferences | language.es | Settings ▸ Language ▸ Español
413
524
  vestra.info | info.about | About Vestra Player… | F1
414
525
  host | quit | Quit | Cmd+Q
415
526
  host | exit-fullscreen | (Leave Fullscreen) | Escape
527
+ host | sidebar | (Toggle Sidebar)
416
528
  vestra.info | info.brief | (Brief Info) | Shift+Tab
417
529
  vestra.playlist | playlist.play | (Play) | Enter, P | panel
418
530
  vestra.playlist | playlist.list-add | (Add Files…) | Ctrl+I | panel
@@ -456,7 +568,12 @@ vestra.playlist | playlist.settings | (Playlist Settings…) | F5 | panel
456
568
  * placeholders (`No Audio Tracks`) are. The player's tests check that this
457
569
  * list agrees with what the player and its built-in plugins register.
458
570
  *
459
- * @example
571
+ * @example Check in a test that a default key is still free
572
+ * The test fails if the player or a built-in plugin already binds Ctrl+K
573
+ * globally, before users ever see a conflict. It collects the default
574
+ * bindings of every built-in command that is not limited to its panel's
575
+ * focus (`focus: null`) and looks for the key the plugin wants; bindings
576
+ * are canonical, so compare them as the player writes them.
460
577
  * ```ts
461
578
  * import assert from "node:assert/strict";
462
579
  * import { BUILTIN_COMMANDS } from "@vestra-player/sdk/testing";
package/dist/index.d.ts CHANGED
@@ -12,7 +12,12 @@
12
12
  * documents the manifest capability it needs. Errors are described at
13
13
  * {@link PluginError}. Tests use `@vestra-player/sdk/testing`.
14
14
  *
15
- * @example
15
+ * @example Add a menu item that shows a message
16
+ * The player gets a Tools ▸ Hello menu item; picking it shows "Hello" on
17
+ * the video. The player imports the entry module and calls `activate` with
18
+ * the plugin's `Plugin` object; `commands.add` registers the item (manifest
19
+ * capability `commands`), and the player calls `run` when the user picks it
20
+ * or presses a key bound to it. `osd.show` needs `ui.osd`.
16
21
  * ```ts
17
22
  * import type { Plugin } from "@vestra-player/sdk";
18
23
  *
@@ -11,11 +11,25 @@
11
11
  * the palette (`slint/palette.slint`); the player's tests check both
12
12
  * palettes against its own.
13
13
  *
14
- * @example
14
+ * @example Check in a test that a colour stays readable in both themes
15
+ * The test fails if the plugin's own highlight colour gets too close to
16
+ * the panel background of either palette. Plugins draw with `Theme` tokens
17
+ * in Slint and the player switches palettes; a colour the plugin picks
18
+ * itself does not switch, so a test compares it with each palette's values
19
+ * (hex strings, `#rrggbb`).
15
20
  * ```ts
21
+ * import assert from "node:assert/strict";
16
22
  * import { THEME_PALETTES } from "@vestra-player/sdk/testing";
17
23
  *
18
- * const accent = THEME_PALETTES["vestra-light"].accent; // "#d4128a"
24
+ * const highlight = "#e07000"; // the plugin's own orange
25
+ * const luminance = (hex: string) => {
26
+ * const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
27
+ * return 0.2126 * r! + 0.7152 * g! + 0.0722 * b!; // rough, without gamma
28
+ * };
29
+ * for (const [name, palette] of Object.entries(THEME_PALETTES)) {
30
+ * const gap = Math.abs(luminance(highlight) - luminance(palette["bg-panel"]!));
31
+ * assert.ok(gap > 0.3, `${name}: highlight too close to bg-panel`);
32
+ * }
19
33
  * ```
20
34
  *
21
35
  * @since 1.5.14
package/dist/palettes.js CHANGED
@@ -11,11 +11,25 @@
11
11
  * the palette (`slint/palette.slint`); the player's tests check both
12
12
  * palettes against its own.
13
13
  *
14
- * @example
14
+ * @example Check in a test that a colour stays readable in both themes
15
+ * The test fails if the plugin's own highlight colour gets too close to
16
+ * the panel background of either palette. Plugins draw with `Theme` tokens
17
+ * in Slint and the player switches palettes; a colour the plugin picks
18
+ * itself does not switch, so a test compares it with each palette's values
19
+ * (hex strings, `#rrggbb`).
15
20
  * ```ts
21
+ * import assert from "node:assert/strict";
16
22
  * import { THEME_PALETTES } from "@vestra-player/sdk/testing";
17
23
  *
18
- * const accent = THEME_PALETTES["vestra-light"].accent; // "#d4128a"
24
+ * const highlight = "#e07000"; // the plugin's own orange
25
+ * const luminance = (hex: string) => {
26
+ * const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
27
+ * return 0.2126 * r! + 0.7152 * g! + 0.0722 * b!; // rough, without gamma
28
+ * };
29
+ * for (const [name, palette] of Object.entries(THEME_PALETTES)) {
30
+ * const gap = Math.abs(luminance(highlight) - luminance(palette["bg-panel"]!));
31
+ * assert.ok(gap > 0.3, `${name}: highlight too close to bg-panel`);
32
+ * }
19
33
  * ```
20
34
  *
21
35
  * @since 1.5.14
@@ -13,7 +13,7 @@ export { HostError } from "./wire.ts";
13
13
  export type { Json, Transport } from "./wire.ts";
14
14
  export { API_VERSION, CAPABILITIES, CAPABILITY_INFO, MESSAGE_LIMIT, } from "./capabilities.ts";
15
15
  export { HOST_CAPABILITIES } from "./host-capabilities.ts";
16
- export { BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "./commands.ts";
16
+ export { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "./commands.ts";
17
17
  export { BINDING_INPUTS } from "./plugin/bindings.ts";
18
18
  export { OSD_LIMITS } from "./plugin/osd.ts";
19
19
  export { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./plugin/services.ts";
@@ -11,7 +11,7 @@ export { createPlugin } from "./plugin/index.js";
11
11
  export { HostError } from "./wire.js";
12
12
  export { API_VERSION, CAPABILITIES, CAPABILITY_INFO, MESSAGE_LIMIT, } from "./capabilities.js";
13
13
  export { HOST_CAPABILITIES } from "./host-capabilities.js";
14
- export { BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "./commands.js";
14
+ export { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "./commands.js";
15
15
  export { BINDING_INPUTS } from "./plugin/bindings.js";
16
16
  export { OSD_LIMITS } from "./plugin/osd.js";
17
17
  export { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./plugin/services.js";