@scarlett-player/gestures 1.8.0 → 1.9.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/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # @scarlett-player/gestures
2
+
3
+ Touch gestures for [Scarlett Player](https://scarlettplayer.com). Double-tap the right of the picture to jump forward, the left to jump back, keep tapping to go further, and single-tap to toggle the controls. Gestures are gated on `pointerType === 'touch'`, so a mouse or pen never triggers any of them and desktop behaviour is unchanged.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @scarlett-player/core @scarlett-player/gestures
9
+ ```
10
+
11
+ `@scarlett-player/core` is a peer dependency. `@scarlett-player/ui` is optional: tap-to-toggle needs it, double-tap seeking does not.
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { createPlayer } from '@scarlett-player/core';
17
+ import { uiPlugin } from '@scarlett-player/ui';
18
+ import { createGesturesPlugin } from '@scarlett-player/gestures';
19
+
20
+ const player = await createPlayer({
21
+ container: '#player',
22
+ src: 'https://example.com/video.m3u8',
23
+ plugins: [uiPlugin(), createGesturesPlugin({ seekSeconds: 10 })],
24
+ });
25
+ ```
26
+
27
+ ## Configuration
28
+
29
+ | Option | Type | Default | Description |
30
+ |---|---|---|---|
31
+ | `enabled` | `boolean \| 'auto'` | `'auto'` | `'auto'` installs the gesture surface when `matchMedia('(any-pointer: coarse)')` matches, so a touchscreen laptop counts. `true` forces it, `false` disables the plugin. No user agent sniffing |
32
+ | `seekSeconds` | `number` | `10` | Seconds moved per seek step |
33
+ | `doubleTapWindowMs` | `number` | `275` | Second tap within this window makes a double tap |
34
+ | `accumulationWindowMs` | `number` | `650` | A further tap in the same zone within this window extends the seek |
35
+ | `zones` | `{ left?, right? }` | `{ left: 0.33, right: 0.33 }` | Fractions of the width given to each seek zone. The middle is inert on purpose |
36
+ | `slopPx` | `number` | `10` | Movement in CSS pixels that turns a tap into a drag and cancels it |
37
+ | `feedback` | `boolean` | `true` | Show the zone highlight and cumulative seconds label |
38
+ | `haptics` | `boolean` | `true` | `navigator.vibrate(10)` on each seek step where supported. iOS Safari ignores it |
39
+ | `tapToToggleControls` | `boolean` | `true` | Let a single tap show or hide the controls |
40
+
41
+ ## Behaviour
42
+
43
+ - Not installed for audio: when `mediaType` is `'audio'` at init the plugin stays inactive.
44
+ - Seeking is refused while Chromecast or AirPlay is active, on live streams with no seekable range, and on VOD until a finite duration is known.
45
+ - On live DVR a seek is clamped to the seekable range. A forward tap at the live edge announces "Already at the live edge" instead of showing a ripple.
46
+ - Each tap in a run adds `seekSeconds`; the label shows the cumulative total. A tap in the opposite zone ends the run rather than reversing it, and a drag or a second finger cancels it.
47
+ - A single tap shows hidden controls immediately. Hiding waits out `doubleTapWindowMs` so a seek is never preceded by the controls blinking away, and nothing hides while paused.
48
+ - The surface never calls `preventDefault` or `stopPropagation`, so document-level click handlers (menus closing, for example) still fire.
49
+
50
+ The plugin exposes `ownsTapInteraction()`, which the UI package checks before running its own show-controls logic on touch. It returns `true` while the plugin is active and `tapToToggleControls` is on.
51
+
52
+ ## Events
53
+
54
+ | Event | Payload | When |
55
+ |---|---|---|
56
+ | `gesture:tap` | `{ zone }` | A single tap landed. `zone` is `'left'`, `'middle'` or `'right'` |
57
+ | `gesture:seek` | `{ direction, seconds, cumulative }` | A seek step moved the playhead. `direction` is `'forward'` or `'backward'` |
58
+
59
+ Each seek step also emits the core `playback:seeking` event with the target time.
60
+
61
+ ## CSS
62
+
63
+ Styles are injected once per document in a `<style id="sp-gestures-styles">`. There are no CSS custom properties.
64
+
65
+ | Class | Element |
66
+ |---|---|
67
+ | `.sp-gestures` | The surface. Absolutely positioned at `z-index: 6`, leaving the bottom 64px to the progress and control bars |
68
+ | `.sp-gestures__zone` | One seek zone, `--left` or `--right`, sized from `zones`. `--active` while feedback is showing |
69
+ | `.sp-gestures__label` | The cumulative seconds label inside a zone |
70
+ | `.sp-gestures__live` | Visually hidden polite live region carrying the announcement |
71
+
72
+ The zone transition is disabled under `prefers-reduced-motion: reduce`.
73
+
74
+ ## Lower level exports
75
+
76
+ `createRecognizer`, `zoneFor` and `DEFAULT_RECOGNIZER_OPTIONS` expose the pure tap state machine, and `GestureOverlay` the DOM surface, for hosts that want to build their own handling on top.
77
+
78
+ ## License
79
+
80
+ MIT
package/dist/index.cjs CHANGED
@@ -178,6 +178,7 @@ function createRecognizer(options = {}) {
178
178
  }
179
179
 
180
180
  // src/overlay.ts
181
+ var import_core = require("@scarlett-player/core");
181
182
  var STYLE_ID = "sp-gestures-styles";
182
183
  var styles = `
183
184
  .sp-gestures {
@@ -250,7 +251,7 @@ var GestureOverlay = class {
250
251
  constructor(container, options) {
251
252
  this.container = container;
252
253
  this.options = options;
253
- this.styleEl = null;
254
+ this.releaseStyles = null;
254
255
  this.hideTimer = null;
255
256
  this.pointerHandler = (event) => {
256
257
  if (event.pointerType !== "touch") return;
@@ -325,8 +326,8 @@ var GestureOverlay = class {
325
326
  this.el.removeEventListener("pointerup", this.pointerHandler);
326
327
  this.el.removeEventListener("pointercancel", this.pointerHandler);
327
328
  this.el.remove();
328
- this.styleEl?.remove();
329
- this.styleEl = null;
329
+ this.releaseStyles?.();
330
+ this.releaseStyles = null;
330
331
  }
331
332
  /** Exposed for tests and for hosts that want to inspect the surface. */
332
333
  getElement() {
@@ -341,17 +342,19 @@ var GestureOverlay = class {
341
342
  zone.appendChild(label);
342
343
  return { zone, label };
343
344
  }
345
+ /**
346
+ * Claim the shared gesture stylesheet.
347
+ *
348
+ * Reference-counted: two players on one page share the sheet, and destroying
349
+ * either used to take it away from the other.
350
+ */
344
351
  injectStyles() {
345
- if (document.getElementById(STYLE_ID)) return;
346
- this.styleEl = document.createElement("style");
347
- this.styleEl.id = STYLE_ID;
348
- this.styleEl.textContent = styles;
349
- document.head.appendChild(this.styleEl);
352
+ this.releaseStyles = (0, import_core.injectSharedStyles)(STYLE_ID, styles);
350
353
  }
351
354
  };
352
355
 
353
356
  // src/version.ts
354
- var PKG_VERSION = true ? "1.7.1" : "0.0.0-dev";
357
+ var PKG_VERSION = true ? "1.9.0" : "0.0.0-dev";
355
358
 
356
359
  // src/index.ts
357
360
  function hasCoarsePointer() {
package/dist/index.d.cts CHANGED
@@ -201,7 +201,7 @@ declare class GestureOverlay {
201
201
  private zones;
202
202
  private labels;
203
203
  private live;
204
- private styleEl;
204
+ private releaseStyles;
205
205
  private hideTimer;
206
206
  private readonly pointerHandler;
207
207
  constructor(container: HTMLElement, options: OverlayOptions);
@@ -220,6 +220,12 @@ declare class GestureOverlay {
220
220
  /** Exposed for tests and for hosts that want to inspect the surface. */
221
221
  getElement(): HTMLElement;
222
222
  private createZone;
223
+ /**
224
+ * Claim the shared gesture stylesheet.
225
+ *
226
+ * Reference-counted: two players on one page share the sheet, and destroying
227
+ * either used to take it away from the other.
228
+ */
223
229
  private injectStyles;
224
230
  }
225
231
 
package/dist/index.d.ts CHANGED
@@ -201,7 +201,7 @@ declare class GestureOverlay {
201
201
  private zones;
202
202
  private labels;
203
203
  private live;
204
- private styleEl;
204
+ private releaseStyles;
205
205
  private hideTimer;
206
206
  private readonly pointerHandler;
207
207
  constructor(container: HTMLElement, options: OverlayOptions);
@@ -220,6 +220,12 @@ declare class GestureOverlay {
220
220
  /** Exposed for tests and for hosts that want to inspect the surface. */
221
221
  getElement(): HTMLElement;
222
222
  private createZone;
223
+ /**
224
+ * Claim the shared gesture stylesheet.
225
+ *
226
+ * Reference-counted: two players on one page share the sheet, and destroying
227
+ * either used to take it away from the other.
228
+ */
223
229
  private injectStyles;
224
230
  }
225
231
 
package/dist/index.js CHANGED
@@ -147,6 +147,7 @@ function createRecognizer(options = {}) {
147
147
  }
148
148
 
149
149
  // src/overlay.ts
150
+ import { injectSharedStyles } from "@scarlett-player/core";
150
151
  var STYLE_ID = "sp-gestures-styles";
151
152
  var styles = `
152
153
  .sp-gestures {
@@ -219,7 +220,7 @@ var GestureOverlay = class {
219
220
  constructor(container, options) {
220
221
  this.container = container;
221
222
  this.options = options;
222
- this.styleEl = null;
223
+ this.releaseStyles = null;
223
224
  this.hideTimer = null;
224
225
  this.pointerHandler = (event) => {
225
226
  if (event.pointerType !== "touch") return;
@@ -294,8 +295,8 @@ var GestureOverlay = class {
294
295
  this.el.removeEventListener("pointerup", this.pointerHandler);
295
296
  this.el.removeEventListener("pointercancel", this.pointerHandler);
296
297
  this.el.remove();
297
- this.styleEl?.remove();
298
- this.styleEl = null;
298
+ this.releaseStyles?.();
299
+ this.releaseStyles = null;
299
300
  }
300
301
  /** Exposed for tests and for hosts that want to inspect the surface. */
301
302
  getElement() {
@@ -310,17 +311,19 @@ var GestureOverlay = class {
310
311
  zone.appendChild(label);
311
312
  return { zone, label };
312
313
  }
314
+ /**
315
+ * Claim the shared gesture stylesheet.
316
+ *
317
+ * Reference-counted: two players on one page share the sheet, and destroying
318
+ * either used to take it away from the other.
319
+ */
313
320
  injectStyles() {
314
- if (document.getElementById(STYLE_ID)) return;
315
- this.styleEl = document.createElement("style");
316
- this.styleEl.id = STYLE_ID;
317
- this.styleEl.textContent = styles;
318
- document.head.appendChild(this.styleEl);
321
+ this.releaseStyles = injectSharedStyles(STYLE_ID, styles);
319
322
  }
320
323
  };
321
324
 
322
325
  // src/version.ts
323
- var PKG_VERSION = true ? "1.7.1" : "0.0.0-dev";
326
+ var PKG_VERSION = true ? "1.9.0" : "0.0.0-dev";
324
327
 
325
328
  // src/index.ts
326
329
  function hasCoarsePointer() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scarlett-player/gestures",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Gestures Plugin for Scarlett Player - double-tap seek zones and tap to toggle controls",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -22,7 +22,7 @@
22
22
  "dist"
23
23
  ],
24
24
  "peerDependencies": {
25
- "@scarlett-player/core": "^1.7.0"
25
+ "@scarlett-player/core": "^1.8.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@vitest/coverage-v8": "^1.6.0",
@@ -30,7 +30,7 @@
30
30
  "tsup": "^8.5.1",
31
31
  "typescript": "^5.3.0",
32
32
  "vitest": "^1.6.0",
33
- "@scarlett-player/core": "1.8.0"
33
+ "@scarlett-player/core": "1.9.0"
34
34
  },
35
35
  "keywords": [
36
36
  "video",