@vestra-player/sdk 1.4.0 → 1.5.1

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,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.1
4
+
5
+ Fix; no API or type changes.
6
+
7
+ - **`storage.json` saves no longer collide.** Every save wrote through the same temporary file (`<name>.json.tmp-<pid>`), and only the writes of one store were queued. Two stores on the same file (the same name opened twice, in one scope or two) could therefore rename each other's temporary file away mid-save, and the save failed with `ENOENT` (on `chmod` or `rename`); on Windows two renames onto the file at once also fail with `EPERM`. Seen intermittently on Windows.
8
+ - Writes to one file are now queued together, whichever store makes them, and land in the order they were made.
9
+ - Each write has a temporary file of its own, made owner-only (`0600`) through its file handle before it gets the store's name, and removed again if the write fails.
10
+ - On Windows, replacing the file is retried for up to 2 s while another program (a virus scanner, the search indexer) briefly holds it open.
11
+
12
+ ## 1.5.0
13
+
14
+ **Own visual identity: the Aurora palettes and the V-Line icons** (player 0.4.0). Additive; `apiVersion` stays `vestra/1`. Plugins that use the additions set `"minPlayerVersion": "0.4.0"`.
15
+
16
+ - **`ThemeId` gains `"vestra-light"`**, a light palette whose video area stays dark. `plugin.appearance.set("vestra-light")` switches every view; `TestHost` accepts it like the player. A player older than 0.4.0 answers it with `vestra` (an id it does not have).
17
+ - **New colours for `vestra`.** The palette is now Aurora, drawn from the logo's full spectrum: magenta accent (`#f72aa6`), cyan for what is playing (`Theme.playing`, `#2bb6ff`, violet before), `Theme.accent-fill` from amber through magenta and violet to cyan, and a visible `Theme.line` (`#2b2734`; it was near-black). Token names are unchanged; code that reads `Theme` follows.
18
+ - **Rounder corners**: `Theme.radius-control` 4px (was 3px), `Theme.radius-panel` 8px (was 6px). Sizes are otherwise unchanged.
19
+ - **`@vestra/icons.slint`** (new library file, `slint/vestra/icons.slint`): `Icon { name: "play"; color: Theme.text; }` draws one icon of the player's own set (round-capped 1.5px lines), and `Icons.commands(name)` / `box-width` / `box-height` / `line` give its path data. The names are listed in **Windows, panels, settings pages, dialogs and the OSD**.
20
+ - **`@vestra/widgets.slint`**: the bar slider's thumb is a white core in a 2px accent ring; the panel slider's track is `Theme.line`; stroked icon buttons use round caps and joins; tooltips have rounded corners. Geometry and API are unchanged.
21
+ - **std-widgets in a light palette**: the player still compiles plugin files with `fluent-dark` and switches the std-widgets colour scheme (`Palette.color-scheme`) to light for `vestra-light`, so `CheckBox`, `ComboBox`, `LineEdit`, `ScrollView` and the other std-widgets controls follow. The player's palette file (`slint/palette.slint`, not part of the library) now imports `std-widgets.slint` for that.
22
+
23
+ ## 1.4.1
24
+
25
+ Documentation only; no code or type changes.
26
+
27
+ - **Built-in menus keep their place** (player 0.3.0). `CommandOptions` and `CommandOptions.order` document the player's menu rule: the seven built-in top-level menus (`@open` … `@settings`) sit at fixed orders (Open 110, Playback 200, Subtitles 300, Video 400, Audio 500, Screen 710, Settings 830), whether a path names them by id or by title, so a plugin's item never moves them. Directly inside a built-in menu, `order` 0 (the default) sorts after every other order, so an item without `order` goes at the end. Players before 0.3.0 move the whole menu to the top for such an item; give it an order in the menu's range there.
28
+ - The Blur walkthrough explains `order` inside the Video menu accordingly.
29
+
3
30
  ## 1.4.0
4
31
 
5
32
  **One palette.** `ThemeId` is now `"vestra"` only: the second palette that earlier versions listed is gone from the player. This is a minor version although a type narrows, because the only plugins it affects compare `plugin.appearance.theme` with, or `set` the removed id:
package/README.md CHANGED
@@ -33,7 +33,13 @@ npm i -D @vestra-player/sdk # Node.js 26 or newer
33
33
  `manifest.json` and `main.ts`:
34
34
 
35
35
  ```json
36
- { "id": "examples.hello", "version": "0.1.0", "apiVersion": "vestra/1", "entry": "main.ts", "capabilities": ["commands", "ui.osd"] }
36
+ {
37
+ "id": "examples.hello",
38
+ "version": "0.1.0",
39
+ "apiVersion": "vestra/1",
40
+ "entry": "main.ts",
41
+ "capabilities": ["commands", "ui.osd"]
42
+ }
37
43
  ```
38
44
 
39
45
  ```ts
@@ -54,34 +60,36 @@ Run `npx vestra-plugin pack` to check and package it, or start the player with `
54
60
 
55
61
  Complete, tested plugins in [`examples/`](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples). The [examples page](https://vestrasys.github.io/vestra-player-sdk/documents/Examples.html) shows each one with a screenshot and a step-by-step walkthrough.
56
62
 
57
- | Example | What you learn | Walkthrough |
58
- | --- | --- | --- |
59
- | [hello](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/hello) | The smallest plugin: a manifest, one menu command, an on-screen message. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Hello.html) |
60
- | [blur](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/blur) | Video filters, a tool window with live controls, a shortcut, a settings page, remembered state. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Blur.html) |
61
- | [signal-generator](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/signal-generator) | Generating video and audio frames yourself, phase-continuous audio, audio filters, stream lifecycle. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Signal_generator.html) |
62
- | [bookmark](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/bookmark) | Reading and controlling playback from a tool window. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Bookmark.html) |
63
- | [panel-menu](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/panel-menu) | A sidebar panel with its own context menu, panel-only keys and next/previous. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Panel_menu.html) |
64
- | [core-ui](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/core-ui) | Menus (radio items, groups, changing titles), repeating keys, the shared sidebar, theme widgets. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Core_UI.html) |
65
- | [markdown](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/markdown) | The Markdown view with local and remote images; the manifest's display fields. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Markdown.html) |
63
+ | Example | What you learn | Walkthrough |
64
+ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
65
+ | [hello](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/hello) | The smallest plugin: a manifest, one menu command, an on-screen message. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Hello.html) |
66
+ | [blur](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/blur) | A video filter driven by one slider in a tool window. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Blur.html) |
67
+ | [signal-generator](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/signal-generator) | Generating video and audio frames yourself: colour bars and a sine tone. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Signal_generator.html) |
68
+ | [bookmark](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/bookmark) | A tool window bound to the playback state, with Mark and Jump buttons. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Bookmark.html) |
69
+ | [panel-menu](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/panel-menu) | A sidebar panel with its own right-click menu. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Panel_menu.html) |
70
+ | [menus](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/menus) | Radio items, separator groups and a menu item whose title changes. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Menus.html) |
71
+ | [theme-widgets](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/theme-widgets) | A tool window drawn with the player's theme and widgets. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Theme_widgets.html) |
72
+ | [markdown](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/markdown) | A window that shows a Markdown document. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Markdown.html) |
66
73
 
67
74
  ## Command line
68
75
 
69
- | Command | Does |
70
- | --- | --- |
71
- | `vestra-plugin check [dir...]` | Checks the manifest, then a strict type check of the plugin and its tests. |
72
- | `vestra-plugin test [dir...]` | Runs `node --test` on `tests/*.test.{ts,mjs}`. |
73
- | `vestra-plugin build [dir...] [--version x.y.z]` | Writes `dist/`: types stripped, files copied, manifest versioned. |
74
- | `vestra-plugin pack [dir...] [--version x.y.z] [--out dir]` | Builds, then packs `dist/` as `<id>-<version>.vxp`. |
76
+ | Command | Does |
77
+ | ----------------------------------------------------------- | -------------------------------------------------------------------------- |
78
+ | `vestra-plugin check [dir...]` | Checks the manifest, then a strict type check of the plugin and its tests. |
79
+ | `vestra-plugin test [dir...]` | Runs `node --test` on `tests/*.test.{ts,mjs}`. |
80
+ | `vestra-plugin build [dir...] [--version x.y.z]` | Writes `dist/`: types stripped, files copied, manifest versioned. |
81
+ | `vestra-plugin pack [dir...] [--version x.y.z] [--out dir]` | Builds, then packs `dist/` as `<id>-<version>.vxp`. |
75
82
 
76
83
  ## Compatibility
77
84
 
78
- | SDK | Plugin API | Vestra Player | Node.js |
79
- | --- | --- | --- | --- |
80
- | 1.0 | `vestra/1` | 0.1 or newer | 22.18 or newer |
81
- | 1.1 | `vestra/1` | 0.2 or newer for the 1.1 additions | 22.18 or newer |
82
- | 1.2 | `vestra/1` | 0.2 or newer | 26 (shipped with the player) |
83
- | 1.3 | `vestra/1` | 0.2 or newer; 0.3 for the 1.3 additions (menu ids, `Theme.danger`, empty video chains) | 26 (shipped with the player) |
85
+ | SDK | Plugin API | Vestra Player | Node.js |
86
+ | --- | ---------- | ----------------------------------------------------------------------------------------- | ---------------------------- |
87
+ | 1.0 | `vestra/1` | 0.1 or newer | 22.18 or newer |
88
+ | 1.1 | `vestra/1` | 0.2 or newer for the 1.1 additions | 22.18 or newer |
89
+ | 1.2 | `vestra/1` | 0.2 or newer | 26 (shipped with the player) |
90
+ | 1.3 | `vestra/1` | 0.2 or newer; 0.3 for the 1.3 additions (menu ids, `Theme.danger`, empty video chains) | 26 (shipped with the player) |
84
91
  | 1.4 | `vestra/1` | 0.2 or newer; 0.3 for the 1.3 additions. `ThemeId` is `"vestra"` only (see the CHANGELOG) | 26 (shipped with the player) |
92
+ | 1.5 | `vestra/1` | 0.2 or newer; 0.3 for the 1.3 additions; 0.4 for the 1.5 additions (`vestra-light`, `@vestra/icons.slint`) | 26 (shipped with the player) |
85
93
 
86
94
  Minor releases only add backwards-compatible API; a breaking change gets a new `apiVersion` and a new major version. `@vestra-player/sdk/runtime` is for the player only and not covered by semver.
87
95
 
@@ -54,7 +54,7 @@ export type CommandDeclaration = {
54
54
  title: string;
55
55
  /** Complete primary menu path INCLUDING its leaf; empty = not in the main menu. */
56
56
  path: string[];
57
- /** Sibling order, ascending; a submenu takes its first command's order. */
57
+ /** Sibling order, ascending; a submenu takes its first command's order (a built-in menu keeps a fixed one). */
58
58
  order: number;
59
59
  /** Separator label: adjacent siblings of different groups get a line. */
60
60
  group: string;
@@ -2,7 +2,7 @@
2
2
  import { failure } from "./runtime.js";
3
3
  import { windowOf } from "./window.js";
4
4
  /** The palettes this SDK knows, the default first. */
5
- export const THEMES = ["vestra"];
5
+ export const THEMES = ["vestra", "vestra-light"];
6
6
  /**
7
7
  * One per plugin: the value comes with the `window.state` read before
8
8
  * activation; with `events`, `appearance.changed` keeps it current once
@@ -4,7 +4,7 @@
4
4
  * run in this plugin's own process.
5
5
  */
6
6
  import { existsSync, readFileSync, renameSync } from "node:fs";
7
- import { chmod, mkdir, rename, stat, writeFile } from "node:fs/promises";
7
+ import { mkdir, open, rename, rm, stat } from "node:fs/promises";
8
8
  import { join, resolve } from "node:path";
9
9
  import { failure } from "./runtime.js";
10
10
  import { systemServices } from "./local.js";
@@ -101,6 +101,54 @@ export function tools(scope) {
101
101
  };
102
102
  }
103
103
  const NAME = /^[a-z0-9-]{1,64}$/;
104
+ /** Writes so far in this process; numbers each write's temporary file. */
105
+ let written = 0;
106
+ /**
107
+ * The pending writes of each file, whichever store they come from: the same
108
+ * name opened twice must not write or rename into one file at once.
109
+ */
110
+ const writes = new Map();
111
+ /** Write `text` to `path` atomically, owner-only (0600). */
112
+ async function writeAtomic(path, text) {
113
+ // A name of its own per write, so no other write renames it away.
114
+ const temporary = `${path}.tmp-${process.pid}-${++written}`;
115
+ await mkdir(join(path, ".."), { recursive: true });
116
+ try {
117
+ // Settings can hold private paths and history. The mode is set through
118
+ // the handle, before the file has its name.
119
+ const file = await open(temporary, "w", 0o600);
120
+ try {
121
+ await file.chmod(0o600);
122
+ await file.writeFile(text);
123
+ }
124
+ finally {
125
+ await file.close();
126
+ }
127
+ await replace(temporary, path);
128
+ }
129
+ catch (error) {
130
+ await rm(temporary, { force: true }).catch(() => { });
131
+ throw error;
132
+ }
133
+ }
134
+ /**
135
+ * Rename over `to`. Windows refuses while another program (a virus scanner,
136
+ * the search indexer) has `to` open for a moment: retry for up to 2 s there.
137
+ */
138
+ async function replace(from, to) {
139
+ for (let attempt = 0;; attempt++)
140
+ try {
141
+ return await rename(from, to);
142
+ }
143
+ catch (error) {
144
+ const code = error.code;
145
+ if (process.platform !== "win32" ||
146
+ !(code === "EPERM" || code === "EACCES" || code === "EBUSY") ||
147
+ attempt >= 20)
148
+ throw error;
149
+ await new Promise((resolve) => setTimeout(resolve, 10 + attempt * 10));
150
+ }
151
+ }
104
152
  class Store {
105
153
  #scope;
106
154
  #path;
@@ -159,17 +207,19 @@ class Store {
159
207
  return this.#writing;
160
208
  this.#dirty = false;
161
209
  const text = `${JSON.stringify(this.#value, null, 2)}\n`;
162
- // One write at a time, in order; the newest value is written last.
163
- this.#writing = this.#writing
210
+ // One write per file at a time, in order; the newest value is written
211
+ // last. The queue is per file, not per store (see `writes`).
212
+ const path = this.#path;
213
+ const write = (writes.get(path) ?? Promise.resolve())
164
214
  .catch(() => { })
165
- .then(async () => {
166
- const temporary = `${this.#path}.tmp-${process.pid}`;
167
- await mkdir(join(this.#path, ".."), { recursive: true });
168
- // Owner-only: settings can hold private paths and history.
169
- await writeFile(temporary, text, { mode: 0o600 });
170
- await chmod(temporary, 0o600);
171
- await rename(temporary, this.#path);
172
- });
215
+ .then(() => writeAtomic(path, text));
216
+ const done = () => {
217
+ if (writes.get(path) === write)
218
+ writes.delete(path);
219
+ };
220
+ writes.set(path, write);
221
+ write.then(done, done);
222
+ this.#writing = write;
173
223
  return this.#writing;
174
224
  }
175
225
  async dispose() {
@@ -376,6 +376,21 @@ export type MenuId = "open" | "playback" | "subtitles" | "video" | "audio" | "sc
376
376
  * ranges: `open` 100–199, `domain` 200–699 (playback, subtitles, video,
377
377
  * audio…), `screen` 700–799, `windows` 800–899, `exit` 900–999.
378
378
  *
379
+ * Built-in menus keep their place. The seven built-in top-level menus
380
+ * ({@link MenuId}) sit at fixed orders, whatever is inside them: Open 110,
381
+ * Playback 200, Subtitles 300, Video 400, Audio 500, Screen 710, Settings
382
+ * 830 (player 0.3.0). This holds whether the path names the menu by id
383
+ * (`["@video", "Blur"]`) or by its title (`["视频", "Blur"]`), so a plugin's
384
+ * item never moves the player's menu. Directly inside a built-in menu,
385
+ * `order` 0 (the default) sorts after every other order: an item without
386
+ * `order` goes at the end, after the player's own items (which all use 100
387
+ * or more). Give a positive order to sit between them (the player's orders
388
+ * are listed in its menu table, doc/product/menus.md), or a negative one to
389
+ * come first. Deeper levels (a plugin's own submenu) have no such rule.
390
+ * Players before 0.3.0 place every submenu at its smallest order, so there
391
+ * an item without `order` moves the whole menu to the top; to avoid that,
392
+ * give it an order in the menu's range (Video: 400–499).
393
+ *
379
394
  * {@link Commands.add} throws `invalid` synchronously for a bad path,
380
395
  * order, group or binding. The same path registered twice, or a path that
381
396
  * is both an item and a submenu, fails with `conflict` (through `ready`).
@@ -406,8 +421,10 @@ export interface CommandOptions {
406
421
  title?: string;
407
422
  /**
408
423
  * Sibling order in the menu, ascending (integer, default 0). A submenu sits
409
- * where its first command sits. Equal orders: plugin id, then declaration order.
410
- * Range ±1,000,000.
424
+ * where its first command sits, except a built-in menu, which keeps its
425
+ * fixed place. Directly inside a built-in menu, 0 (the default) sorts last.
426
+ * Equal orders: plugin id, then declaration order. Range ±1,000,000. See
427
+ * {@link CommandOptions} for the menu order rules.
411
428
  */
412
429
  order?: number;
413
430
  /** Separator label: a line is drawn between adjacent items of different groups. Default `"domain"`. */
@@ -1212,8 +1229,17 @@ export interface SlotState {
1212
1229
  *
1213
1230
  * @capability ui.mount; `events` to follow changes made elsewhere.
1214
1231
  *
1215
- * @example A "Show sidebar" menu item that works with any sidebar panel (examples/core-ui)
1216
- * {@includeCode ../../examples/core-ui/main.ts#sidebar}
1232
+ * @example A "Show sidebar" menu item that works with any sidebar panel
1233
+ * ```ts
1234
+ * const sidebar = plugin.panels.sidebar;
1235
+ * plugin.commands.add({
1236
+ * id: "sidebar",
1237
+ * path: ["Tools", "Show sidebar"],
1238
+ * checked: () => sidebar.state?.preferred ?? false,
1239
+ * enabled: () => sidebar.state !== null,
1240
+ * run: () => sidebar.toggle(),
1241
+ * });
1242
+ * ```
1217
1243
  *
1218
1244
  * @see {@link Panels}, {@link SlotState}
1219
1245
  * @since 1.0.0
@@ -1313,8 +1339,17 @@ export interface PanelSlot {
1313
1339
  *
1314
1340
  * @capability ui.mount
1315
1341
  *
1316
- * @example
1317
- * {@includeCode ../../examples/core-ui/main.ts#sidebar}
1342
+ * @example A "Detach sidebar" menu item
1343
+ * ```ts
1344
+ * const sidebar = plugin.panels.sidebar;
1345
+ * plugin.commands.add({
1346
+ * id: "detach",
1347
+ * path: ["Tools", "Detach sidebar"],
1348
+ * checked: () => sidebar.state?.detached ?? false,
1349
+ * enabled: () => sidebar.state !== null,
1350
+ * run: () => sidebar.setDetached(!sidebar.state?.detached),
1351
+ * });
1352
+ * ```
1318
1353
  *
1319
1354
  * @since 1.0.0
1320
1355
  * @category UI
@@ -1562,19 +1597,20 @@ export interface Drops {
1562
1597
  provide(handler: DropHandler): Disposable;
1563
1598
  }
1564
1599
  /**
1565
- * The player's palettes. Today there is one, `vestra` (the default); a later
1566
- * version may add more (a light palette, for example), so read colours from
1567
- * `Theme` instead of branching on the id.
1600
+ * The player's palettes: `vestra`, the dark default, and `vestra-light`
1601
+ * (player 0.4.0, SDK 1.5.0), a light palette whose video area stays dark.
1602
+ * Both draw on the logo's colours. Read colours from `Theme` instead of
1603
+ * branching on the id: a later version may add palettes.
1568
1604
  *
1569
1605
  * @remarks
1570
- * SDK 1.4.0 removed a second palette that earlier versions listed. The player
1571
- * treats an id it does not have as `vestra`, so plugins built with an older
1572
- * SDK that still ask for it keep working.
1606
+ * The player treats an id it does not have as `vestra`: plugins built with an
1607
+ * older SDK that still ask for a palette removed in SDK 1.4.0 keep working,
1608
+ * and a player older than 0.4.0 answers `vestra-light` with `vestra`.
1573
1609
  *
1574
1610
  * @since 1.0.0
1575
1611
  * @category UI
1576
1612
  */
1577
- export type ThemeId = "vestra";
1613
+ export type ThemeId = "vestra" | "vestra-light";
1578
1614
  /**
1579
1615
  * The player's palette, which every view uses. `plugin.appearance`.
1580
1616
  *
@@ -1584,7 +1620,9 @@ export type ThemeId = "vestra";
1584
1620
  * and again when the palette changes. Slint code that reads its colours from
1585
1621
  * `Theme` (`import { Theme } from "@vestra/theme.slint"`) follows it with no
1586
1622
  * plugin code; hard-coded colours do not. Only colours change: corner radii,
1587
- * font sizes and control sizes are the same in every palette.
1623
+ * font sizes and control sizes are the same in every palette. A light palette
1624
+ * also switches the std-widgets controls (`std-widgets.slint`) to their light
1625
+ * colours, and keeps the video area dark.
1588
1626
  *
1589
1627
  * The player saves the choice; it stays after the plugin that set it stops.
1590
1628
  * Tell the user about a change yourself (for example with
@@ -1593,14 +1631,14 @@ export type ThemeId = "vestra";
1593
1631
  * @capability None to read `theme`; `events` to follow changes; `ui.theme`
1594
1632
  * to change it.
1595
1633
  *
1596
- * @example A menu item that selects the palette
1634
+ * @example A menu item that selects the light palette
1597
1635
  * ```ts
1598
1636
  * plugin.commands.add({
1599
- * id: "vestra-colours",
1600
- * path: ["@settings", "Vestra colours"],
1637
+ * id: "light-colours",
1638
+ * path: ["@settings", "Light colours"],
1601
1639
  * radio: true,
1602
- * checked: () => plugin.appearance.theme === "vestra",
1603
- * run: () => plugin.appearance.set("vestra"),
1640
+ * checked: () => plugin.appearance.theme === "vestra-light",
1641
+ * run: () => plugin.appearance.set("vestra-light"),
1604
1642
  * });
1605
1643
  * ```
1606
1644
  *
@@ -1657,7 +1695,7 @@ export interface Appearance {
1657
1695
  *
1658
1696
  * @example
1659
1697
  * ```ts
1660
- * await plugin.appearance.set("vestra");
1698
+ * await plugin.appearance.set("vestra-light");
1661
1699
  * ```
1662
1700
  *
1663
1701
  * @since 1.0.0
@@ -1694,12 +1732,59 @@ export type SettingsCategory = "general" | "playback" | "subtitles" | "video" |
1694
1732
  *
1695
1733
  * @capability ui.settings
1696
1734
  *
1697
- * @example Two preferences on a page (examples/blur)
1698
- * {@includeCode ../../examples/blur/main.ts#settings-page}
1735
+ * @example Two preferences on a page, kept in a store
1736
+ * ```ts
1737
+ * const store = plugin.storage.json("blur", {
1738
+ * defaults: { restoreOnStart: false, messages: true },
1739
+ * });
1740
+ * const page = plugin.settings.page("ui/settings.slint", {
1741
+ * id: "blur",
1742
+ * category: "video",
1743
+ * title: "Blur",
1744
+ * });
1745
+ * page.bind(() => ({
1746
+ * restoreOnStart: store.value.restoreOnStart,
1747
+ * messages: store.value.messages,
1748
+ * }));
1749
+ * page.on("restoreToggled", (on: boolean) =>
1750
+ * store.save({ ...store.value, restoreOnStart: on }),
1751
+ * );
1752
+ * page.on("messagesToggled", (on: boolean) =>
1753
+ * store.save({ ...store.value, messages: on }),
1754
+ * );
1755
+ * ```
1756
+ *
1757
+ * `ui/settings.slint` exports the bridge global and the layout:
1699
1758
  *
1700
- * `ui/settings.slint`:
1759
+ * ```slint
1760
+ * import { CheckBox } from "std-widgets.slint";
1761
+ * import { Theme } from "@vestra/theme.slint";
1701
1762
  *
1702
- * {@includeCode ../../examples/blur/ui/settings.slint}
1763
+ * export global BlurPage {
1764
+ * in-out property <bool> restore-on-start;
1765
+ * in-out property <bool> messages: true;
1766
+ * callback restore-toggled(bool);
1767
+ * callback messages-toggled(bool);
1768
+ * }
1769
+ *
1770
+ * export component BlurSettings inherits Rectangle {
1771
+ * VerticalLayout {
1772
+ * padding: Theme.dialog-padding;
1773
+ * spacing: 6px;
1774
+ * alignment: start;
1775
+ * CheckBox {
1776
+ * text: "Turn blur back on when the player starts";
1777
+ * checked <=> BlurPage.restore-on-start;
1778
+ * toggled => { BlurPage.restore-toggled(self.checked); }
1779
+ * }
1780
+ * CheckBox {
1781
+ * text: "Show on-screen messages";
1782
+ * checked <=> BlurPage.messages;
1783
+ * toggled => { BlurPage.messages-toggled(self.checked); }
1784
+ * }
1785
+ * }
1786
+ * }
1787
+ * ```
1703
1788
  *
1704
1789
  * @see {@link Settings.page}, [Windows, panels, settings pages, dialogs and the OSD](../../docs/concepts/ui.md)
1705
1790
  * @since 1.0.0
@@ -1834,7 +1919,7 @@ export interface Settings {
1834
1919
  * @throws `capability` (rejects) The manifest does not declare
1835
1920
  * `ui.settings`.
1836
1921
  *
1837
- * @example An "Options…" button in a tool window (examples/blur)
1922
+ * @example An "Options…" button in a tool window
1838
1923
  * ```ts
1839
1924
  * const controls = plugin.windows.create("ui/window.slint");
1840
1925
  * controls.on("openOptions", () => plugin.settings.show("blur"));
@@ -3031,8 +3116,14 @@ export interface OsdPanel extends Disposable {
3031
3116
  *
3032
3117
  * @capability ui.osd
3033
3118
  *
3034
- * @example On-screen messages that the user can turn off (examples/blur)
3035
- * {@includeCode ../../examples/blur/main.ts#osd}
3119
+ * @example On-screen messages that the user can turn off
3120
+ * ```ts
3121
+ * const store = plugin.storage.json("prefs", { defaults: { messages: true } });
3122
+ * const say = (text: string) => {
3123
+ * if (store.value.messages) void plugin.osd.show(text);
3124
+ * };
3125
+ * say("Blur on");
3126
+ * ```
3036
3127
  *
3037
3128
  * @see {@link OsdPanel}, [Windows, panels, settings pages, dialogs and the OSD](../../docs/concepts/ui.md)
3038
3129
  * @since 1.0.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vestra-player/sdk",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Plugin API types, test host and plugin CLI for Vestra Player (apiVersion vestra/1)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -1,32 +1,42 @@
1
1
  // The player's colour palette (R-H6) and per-system fonts. Not part of the
2
2
  // plugin library: the player sets these `in` properties on every view;
3
3
  // `Theme` in @vestra/theme.slint reads them as `out` properties. Defaults:
4
- // Vestra colours, the macOS fonts.
4
+ // the Aurora colours of the `vestra` palette, the macOS fonts.
5
+ import { Palette } from "std-widgets.slint";
6
+
5
7
  export global VestraPalette {
6
- in property <color> bg-bar: #17141c;
7
- in property <color> bg-video: #0b0a0f;
8
- in property <color> bg-list: #1c1922;
9
- in property <color> bg-panel: #221e29;
10
- in property <color> line: #07060a;
11
- in property <color> hover-bg: #2a2533;
12
- in property <color> press-bg: #120f16;
13
- in property <color> selected-bg: #2f2938;
14
- in property <color> text: #f2eff7;
15
- in property <color> text-dim: #8a8496;
16
- in property <color> text-list: #b8b2c4;
17
- in property <color> text-played: #6f6a7a;
18
- in property <color> accent: #fa139c;
19
- in property <color> accent-press: #c40f7a;
20
- in property <color> accent-disabled: #5c1a42;
21
- in property <color> icon-disabled: #3a3542;
22
- in property <color> playing: #a24bff;
23
- in property <color> button-bg: #322c3b;
24
- in property <color> button-hover: #3d3648;
25
- in property <color> button-press: #26212e;
26
- in property <color> button-disabled: #1d1a23;
8
+ in property <color> bg-bar: #16141c;
9
+ in property <color> bg-video: #0a090e;
10
+ in property <color> bg-list: #1a1821;
11
+ in property <color> bg-panel: #201d28;
12
+ in property <color> line: #2b2734;
13
+ in property <color> hover-bg: #28232f;
14
+ in property <color> press-bg: #110f16;
15
+ in property <color> selected-bg: #2f2840;
16
+ in property <color> text: #f5f3fa;
17
+ in property <color> text-dim: #8e88a0;
18
+ in property <color> text-list: #bdb8cb;
19
+ in property <color> text-played: #6e6980;
20
+ in property <color> accent: #f72aa6;
21
+ in property <color> accent-press: #c41a83;
22
+ in property <color> accent-disabled: #5a1d48;
23
+ in property <color> icon-disabled: #3b3646;
24
+ in property <color> playing: #2bb6ff;
25
+ in property <color> button-bg: #2f2a3b;
26
+ in property <color> button-hover: #3a3448;
27
+ in property <color> button-press: #25202e;
28
+ in property <color> button-disabled: #1b1922;
27
29
  // Errors and destructive actions (failed status, delete).
28
- in property <color> danger: #ff5a4a;
30
+ in property <color> danger: #ff5a5f;
29
31
  // The system's monospace family (platform::monospace_font).
30
32
  in property <string> font-mono: "Menlo";
31
- in property <brush> accent-fill: @linear-gradient(90deg, #fdb506 0%, #fc6f3b 25%, #fa139c 60%, #911efc 100%);
33
+ // Each palette's progress gradient, logo amber → magenta → violet → cyan.
34
+ // The player copies one into `accent-fill` (Slint's Rust API cannot
35
+ // build gradients).
36
+ out property <brush> fill-vestra: @linear-gradient(90deg, #fcc402 0%, #fb5a35 20%, #fa0aa8 45%, #7a0afa 72%, #10a8fc 100%);
37
+ out property <brush> fill-vestra-light: @linear-gradient(90deg, #f5a700 0%, #f0442f 20%, #e0089a 45%, #6a08e8 72%, #0a95e6 100%);
38
+ in property <brush> accent-fill: self.fill-vestra;
39
+ // The std-widgets (fluent) controls follow the palette: the player sets
40
+ // `light` for a light palette.
41
+ in-out property <ColorScheme> color-scheme <=> Palette.color-scheme;
32
42
  }
@@ -0,0 +1,99 @@
1
+ // The V-Line icon set (doc/product/ui-spec.md §2.4): round-capped, round-joined
2
+ // lines, 1.5px (1.25px for the small glyphs), never filled. The V of the
3
+ // logo recurs: « » for back and forward, ∧ ∨ for moving rows, a small V for
4
+ // every menu, list and scroll arrow.
5
+ // import { Icon } from "@vestra/icons.slint";
6
+ // Icon { name: "play"; color: Theme.text; }
7
+ // Each icon is drawn in its own box (`Icons.box-width` × `Icons.box-height`),
8
+ // which is also the Icon's default size; an unknown name draws nothing.
9
+ import { Theme } from "theme.slint";
10
+
11
+ export global Icons {
12
+ // Path data in the icon's box.
13
+ public pure function commands(name: string) -> string {
14
+ if name == "play" { return "M 2.2 2.0 L 10.9 7 L 2.2 12.0 Z"; }
15
+ if name == "pause" { return "M 1.75 1.75 H 4.5 V 12.25 H 1.75 Z M 7.5 1.75 H 10.25 V 12.25 H 7.5 Z"; }
16
+ if name == "stop" { return "M 3.6 1.5 H 8.4 Q 10.5 1.5 10.5 3.6 V 8.4 Q 10.5 10.5 8.4 10.5 H 3.6 Q 1.5 10.5 1.5 8.4 V 3.6 Q 1.5 1.5 3.6 1.5 Z"; }
17
+ if name == "back" { return "M 6.5 1.25 L 2.5 5 L 6.5 8.75 M 12 1.25 L 8 5 L 12 8.75"; }
18
+ if name == "forward" { return "M 2 1.25 L 6 5 L 2 8.75 M 7.5 1.25 L 11.5 5 L 7.5 8.75"; }
19
+ if name == "open" { return "M 1.5 3.2 Q 1.5 1.5 3.2 1.5 H 5.2 L 6.8 3.2 H 10.8 Q 12.5 3.2 12.5 4.9 V 8.8 Q 12.5 10.5 10.8 10.5 H 3.2 Q 1.5 10.5 1.5 8.8 Z"; }
20
+ if name == "volume" { return "M 1 3.6 H 3 L 6 1.2 V 8.8 L 3 6.4 H 1 Z M 8.6 3.4 Q 9.7 5 8.6 6.6 M 10.6 1.6 Q 12.6 5 10.6 8.4"; }
21
+ if name == "mute" { return "M 1 3.6 H 3 L 6 1.2 V 8.8 L 3 6.4 H 1 Z M 8.8 3.4 L 12 6.6 M 12 3.4 L 8.8 6.6"; }
22
+ if name == "playlist" { return "M 1.25 1.5 H 11.75 M 1.25 5 H 11.75 M 1.25 8.5 H 6.5 M 9.2 7.2 L 10.5 8.5 L 11.8 7.2"; }
23
+ if name == "detach" { return "M 5.5 1.5 H 3.3 Q 1.5 1.5 1.5 3.3 V 8.7 Q 1.5 10.5 3.3 10.5 H 8.7 Q 10.5 10.5 10.5 8.7 V 6.5 M 8.5 1.5 H 12.5 V 5.5 M 12.5 1.5 L 6.5 7.5"; }
24
+ if name == "attach" { return "M 5.5 1.5 H 3.3 Q 1.5 1.5 1.5 3.3 V 8.7 Q 1.5 10.5 3.3 10.5 H 8.7 Q 10.5 10.5 10.5 8.7 V 6.5 M 12.5 1.5 L 7 7 M 7 3.5 V 7 H 10.5"; }
25
+ if name == "close" { return "M 3.5 3 L 10.5 10 M 10.5 3 L 3.5 10"; }
26
+ if name == "failed" { return "M 5 1.3 L 9.1 8.7 H 0.9 Z M 5 4.1 V 5.8 M 5 7.3 V 7.35"; }
27
+ if name == "move-top" { return "M 2.5 1.75 H 11.5 M 3 9.75 L 7 5.75 L 11 9.75"; }
28
+ if name == "move-up" { return "M 3 8 L 7 4 L 11 8"; }
29
+ if name == "move-down" { return "M 3 4 L 7 8 L 11 4"; }
30
+ if name == "move-bottom" { return "M 3 2.25 L 7 6.25 L 11 2.25 M 2.5 10.25 H 11.5"; }
31
+ if name == "search" { return "M 9 5.25 A 3.75 3.75 0 1 1 1.5 5.25 A 3.75 3.75 0 1 1 9 5.25 Z M 8 8 L 11.25 10.75"; }
32
+ if name == "chapter" { return "M 2.5 11 V 1.2 M 2.5 1.6 H 9.6 L 7.9 4.1 L 9.6 6.6 H 2.5"; }
33
+ if name == "bookmark" { return "M 3.2 1.2 H 8.8 V 10.6 L 6 8.3 L 3.2 10.6 Z"; }
34
+ if name == "check-task" { return "M 2.75 5.75 L 4.75 7.75 L 8.25 3.25"; }
35
+ if name == "check-box" { return "M 1.5 4.75 L 3.6 6.75 L 7.5 2.25"; }
36
+ if name == "menu-check" { return "M 4 7.25 L 6.25 9.5 L 10 4.75"; }
37
+ if name == "submenu" { return "M 1.25 1.25 L 3.75 4 L 1.25 6.75"; }
38
+ if name == "scroll-up" { return "M 1.25 3.75 L 4 1.25 L 6.75 3.75"; }
39
+ if name == "scroll-down" { return "M 1.25 1.25 L 4 3.75 L 6.75 1.25"; }
40
+ if name == "caret-down" { return "M 1.25 1.25 L 4 3.75 L 6.75 1.25"; }
41
+ if name == "sort-asc" { return "M 1 3.25 L 3.5 0.75 L 6 3.25"; }
42
+ if name == "sort-desc" { return "M 1 0.75 L 3.5 3.25 L 6 0.75"; }
43
+ if name == "tag-remove" { return "M 1.5 1.5 L 5.5 5.5 M 5.5 1.5 L 1.5 5.5"; }
44
+ if name == "preview-play" { return "M 1.5 1.3 L 6.9 4.5 L 1.5 7.7 Z"; }
45
+ if name == "add" { return "M 5 1.5 V 8.5 M 1.5 5 H 8.5"; }
46
+ if name == "remove" { return "M 1.5 5 H 8.5"; }
47
+ if name == "sort" { return "M 3 1.5 V 8.5 M 1.2 6.7 L 3 8.5 L 4.8 6.7 M 7 8.5 V 1.5 M 5.2 3.3 L 7 1.5 L 8.8 3.3"; }
48
+ return "";
49
+ }
50
+ // Line width.
51
+ public pure function line(name: string) -> length {
52
+ if name == "failed" || name == "chapter" || name == "bookmark" || name == "submenu" || name == "scroll-up" || name == "scroll-down" || name == "caret-down" || name == "tag-remove" || name == "preview-play" { return 1.25px; }
53
+ if name == "sort-asc" || name == "sort-desc" { return 1.1px; }
54
+ return 1.5px;
55
+ }
56
+ // The box the commands are drawn in.
57
+ public pure function box-width(name: string) -> length {
58
+ if name == "play" || name == "pause" || name == "stop" || name == "chapter" || name == "bookmark" { return 12px; }
59
+ if name == "failed" || name == "add" || name == "remove" || name == "sort" { return 10px; }
60
+ if name == "scroll-up" || name == "scroll-down" || name == "caret-down" || name == "preview-play" { return 8px; }
61
+ if name == "volume" || name == "mute" || name == "playlist" { return 13px; }
62
+ if name == "sort-asc" || name == "sort-desc" || name == "tag-remove" { return 7px; }
63
+ if name == "check-task" { return 11px; }
64
+ if name == "check-box" { return 9px; }
65
+ if name == "submenu" { return 5px; }
66
+ return 14px;
67
+ }
68
+ public pure function box-height(name: string) -> length {
69
+ if name == "back" || name == "forward" || name == "volume" || name == "mute" || name == "playlist" || name == "failed" || name == "add" || name == "remove" || name == "sort" { return 10px; }
70
+ if name == "play" || name == "pause" || name == "menu-check" { return 14px; }
71
+ if name == "scroll-up" || name == "scroll-down" || name == "caret-down" { return 5px; }
72
+ if name == "check-box" || name == "preview-play" { return 9px; }
73
+ if name == "sort-asc" || name == "sort-desc" { return 4px; }
74
+ if name == "close" { return 13px; }
75
+ if name == "check-task" { return 11px; }
76
+ if name == "submenu" { return 8px; }
77
+ if name == "tag-remove" { return 7px; }
78
+ return 12px;
79
+ }
80
+ }
81
+
82
+ // One icon, stroked in `color`; scaled when the caller sets another size.
83
+ export component Icon {
84
+ in property <string> name;
85
+ in property <brush> color: Theme.text;
86
+ width: Icons.box-width(root.name);
87
+ height: Icons.box-height(root.name);
88
+ Path {
89
+ width: 100%;
90
+ height: 100%;
91
+ viewbox-width: Icons.box-width(root.name) / 1px;
92
+ viewbox-height: Icons.box-height(root.name) / 1px;
93
+ commands: Icons.commands(root.name);
94
+ stroke: root.color;
95
+ stroke-width: Icons.line(root.name);
96
+ stroke-line-cap: round;
97
+ stroke-line-join: round;
98
+ }
99
+ }
@@ -6,6 +6,7 @@
6
6
  // open (usually `plugin.system.openUrl`).
7
7
  import { ScrollView } from "std-widgets.slint";
8
8
  import { Theme } from "theme.slint";
9
+ import { Icon } from "icons.slint";
9
10
 
10
11
  // One table row (cells are plain text).
11
12
  export struct MarkdownRow {
@@ -86,18 +87,15 @@ component MarkdownBlockView {
86
87
  y: 3px;
87
88
  width: 11px;
88
89
  height: 11px;
89
- border-width: 1px;
90
- border-radius: 2px;
90
+ border-width: block.task == 2 ? 0px : 1px;
91
+ border-radius: 3px;
91
92
  border-color: Theme.text-dim;
92
93
  background: block.task == 2 ? Theme.accent : transparent;
93
- if block.task == 2: Path {
94
+ if block.task == 2: Icon {
94
95
  width: 100%;
95
96
  height: 100%;
96
- viewbox-width: 11;
97
- viewbox-height: 11;
98
- commands: "M 2.5 5.5 L 4.5 7.5 L 8.5 3";
99
- stroke: Theme.text;
100
- stroke-width: 1.5px;
97
+ name: "check-task";
98
+ color: #ffffff;
101
99
  }
102
100
  }
103
101
  }
@@ -1,7 +1,8 @@
1
1
  // Vestra theme tokens (doc/product/ui-spec.md §2.1–2.3). Import from any
2
2
  // plugin or host Slint file: import { Theme } from "@vestra/theme.slint";
3
- // Colours follow the player's palette (R-H6: Vestra, set on every
4
- // view through ../palette.slint, which is not part of this library). They are
3
+ // Colours follow the player's palette (R-H6: `vestra`, the dark default, or
4
+ // `vestra-light`, set on every view through ../palette.slint, which is not
5
+ // part of this library). They are
5
6
  // `out` properties: Slint code reads them but cannot assign them. So is
6
7
  // font-mono (set per system). Corner radii, font sizes and control sizes
7
8
  // stay constants.
@@ -9,8 +10,10 @@
9
10
  import { VestraPalette } from "../palette.slint";
10
11
 
11
12
  export global Theme {
12
- // Colours: Vestra identity drawn from the logo (amber → magenta → violet on a
13
- // violet-tinted near-black). Sizes below are the player's control geometry
13
+ // Colours: the Aurora palette drawn from the logo's full spectrum (magenta
14
+ // accent, cyan for what is playing, amber → magenta → violet → cyan fills;
15
+ // violet-tinted near-black, or near-white in vestra-light, whose video area
16
+ // stays dark). Sizes below are the player's control geometry
14
17
  // (doc/product/ui-spec.md §2.3), the same whatever the palette.
15
18
  out property <color> bg-bar: VestraPalette.bg-bar;
16
19
  out property <color> bg-video: VestraPalette.bg-video;
@@ -35,11 +38,12 @@ export global Theme {
35
38
  out property <color> button-disabled: VestraPalette.button-disabled;
36
39
  // Errors and destructive actions: a failed status line, a Delete button.
37
40
  out property <color> danger: VestraPalette.danger;
38
- // Brand gradient for "progress" fills (seek bar, volume, sliders).
41
+ // Brand gradient for "progress" fills (seek bar, volume, sliders, accents).
39
42
  out property <brush> accent-fill: VestraPalette.accent-fill;
40
- // Soft corners echo the logo.
41
- out property <length> radius-control: 3px;
42
- out property <length> radius-panel: 6px;
43
+ // Soft corners echo the logo: controls, menu rows and hover plates use
44
+ // radius-control; menus, panels and the OSD radius-panel.
45
+ out property <length> radius-control: 4px;
46
+ out property <length> radius-panel: 8px;
43
47
  // §2.2 fonts. No default family: every window uses the system UI font,
44
48
  // and Slint falls back per character for scripts a font lacks (CJK in
45
49
  // font-mono included). font-mono is the system's own monospace family,
@@ -1,6 +1,7 @@
1
1
  // Compact controls built on the Vestra theme (core-ui-design.md §2.1):
2
2
  // import { Button, Slider, ToolTipArea, UrlInputRow } from "@vestra/widgets.slint";
3
- // Other controls come from std-widgets, which the host compiles as fluent-dark.
3
+ // Icons: @vestra/icons.slint. Other controls come from std-widgets, which the
4
+ // host compiles as fluent-dark and switches to light for a light palette.
4
5
  import { TextEdit } from "std-widgets.slint";
5
6
  import { Theme } from "theme.slint";
6
7
 
@@ -49,6 +50,7 @@ export component ToolTipArea {
49
50
  background: Theme.bg-panel;
50
51
  border-color: Theme.line;
51
52
  border-width: 1px;
53
+ border-radius: Theme.radius-control;
52
54
  HorizontalLayout {
53
55
  padding-left: 5px; padding-right: 5px; padding-top: 2px; padding-bottom: 2px;
54
56
  Text { text: root.text; color: Theme.text; font-size: Theme.font-body; }
@@ -59,7 +61,8 @@ export component ToolTipArea {
59
61
 
60
62
  // Dialog button: text (21px, or 18px when `small`), or an icon button when
61
63
  // `text` is empty and `icon` holds Path data drawn in an icon-width × icon-height
62
- // viewbox (filled, or 1.5px lines when `stroked`). The caller sizes icon buttons.
64
+ // viewbox (filled, or 1.5px round-capped lines when `stroked`, as the
65
+ // @vestra/icons.slint set). The caller sizes icon buttons.
63
66
  export component Button {
64
67
  in property <string> text;
65
68
  in property <string> icon;
@@ -112,6 +115,8 @@ export component Button {
112
115
  fill: root.stroked ? transparent : root.tint;
113
116
  stroke: root.stroked ? root.tint : transparent;
114
117
  stroke-width: root.stroked ? 1.5px : 0px;
118
+ stroke-line-cap: round;
119
+ stroke-line-join: round;
115
120
  }
116
121
  touch := TouchArea {
117
122
  enabled: root.enabled;
@@ -119,8 +124,9 @@ export component Button {
119
124
  }
120
125
  }
121
126
 
122
- // 2px line slider. Bar style (default): 8px round thumb, accent fill.
123
- // Panel style (`panel: true`): 14×12 thumb, blue fill on a dark track.
127
+ // 2px line slider. Bar style (default): 8px round thumb, a white core in a
128
+ // 2px accent ring, gradient fill. Panel style (`panel: true`): 14×12 thumb,
129
+ // gradient fill. The track is `Theme.line` in both.
124
130
  export component Slider {
125
131
  in-out property <float> value;
126
132
  in property <float> minimum: 0;
@@ -146,7 +152,8 @@ export component Slider {
146
152
  Rectangle {
147
153
  y: (parent.height - 2px) / 2;
148
154
  height: 2px;
149
- background: root.panel ? Theme.bg-bar : Theme.line;
155
+ border-radius: 1px;
156
+ background: Theme.line;
150
157
  }
151
158
  Rectangle {
152
159
  y: (parent.height - 2px) / 2;
@@ -162,10 +169,9 @@ export component Slider {
162
169
  width: root.thumb-width;
163
170
  height: root.thumb-height;
164
171
  border-radius: root.panel ? Theme.radius-control : self.height / 2;
165
- background: !root.enabled ? Theme.icon-disabled
166
- : root.panel ? Theme.text
167
- : touch.pressed ? Theme.accent-press
168
- : touch.has-hover ? Theme.accent : Theme.text;
172
+ background: !root.enabled ? Theme.icon-disabled : root.panel ? Theme.text : #ffffff;
173
+ border-width: root.panel || !root.enabled ? 0px : 2px;
174
+ border-color: touch.pressed ? Theme.accent-press : Theme.accent;
169
175
  }
170
176
  touch := TouchArea {
171
177
  enabled: root.enabled;