@pie-players/pie-players-shared 1.0.0-beta.3 → 1.0.0-beta.4

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.
@@ -557,6 +557,10 @@ export interface ToolParameterMap {
557
557
  pitch?: number;
558
558
  volume?: number;
559
559
  };
560
+ /** `zoom` is the lens's starting magnification, 2 meaning twice the size. */
561
+ magnification: {
562
+ zoom?: number;
563
+ };
560
564
  }
561
565
  /** The parameter shape of tool `K`: its {@link ToolParameterMap} entry, else open. */
562
566
  export type ToolParametersFor<K extends string> = K extends keyof ToolParameterMap ? ToolParameterMap[K] : Record<string, unknown>;
@@ -582,6 +586,14 @@ export interface Env {
582
586
  mode: PlayerMode;
583
587
  role: PlayerRole;
584
588
  partialScoring?: boolean;
589
+ /**
590
+ * BCP 47 tag of the language the item's content is written in, when the
591
+ * delivery knows it. The player reflects it onto the content subtree as
592
+ * `lang`/`dir`; a host that emits nothing leaves the content unstamped.
593
+ * The interface locale never travels here — it is the player's `locale`
594
+ * attribute and the `pie.interfaceLocale` context.
595
+ */
596
+ locale?: string;
585
597
  }
586
598
  /** The `detail` of the `player-error` event `pie-item-player` dispatches. */
587
599
  export interface PieItemPlayerErrorDetail {
@@ -24,4 +24,16 @@ type FocusTrapOptions = {
24
24
  * and restores prior focus when the trap is removed.
25
25
  */
26
26
  export declare function createFocusTrap(container: HTMLElement, options?: FocusTrapOptions): () => void;
27
+ /**
28
+ * Returns focus to where it was before a tool took it, for a tool that closes
29
+ * itself: the tool's element leaves the DOM on close, which drops focus to the
30
+ * document body. `capture` runs as the tool takes focus and `restore` as it
31
+ * closes, before its element goes.
32
+ */
33
+ export declare function createFocusReturn(): {
34
+ /** Records the focused element, unless focus is already inside `container`. */
35
+ capture: (container: Node) => void;
36
+ /** Focuses the recorded element, if it is still in the document. */
37
+ restore: () => void;
38
+ };
27
39
  export {};
@@ -44,7 +44,11 @@ export function createFocusTrap(container, options = {}) {
44
44
  const wrap = options.wrap ?? true;
45
45
  const onKeydown = (event) => {
46
46
  if (event.key === "Escape") {
47
- options.onEscape?.();
47
+ if (!options.onEscape)
48
+ return;
49
+ // Claimed, so a page-wide Escape listener does not also act on it.
50
+ event.preventDefault();
51
+ options.onEscape();
48
52
  return;
49
53
  }
50
54
  if (event.key !== "Tab")
@@ -94,3 +98,37 @@ export function createFocusTrap(container, options = {}) {
94
98
  }
95
99
  };
96
100
  }
101
+ /**
102
+ * Returns focus to where it was before a tool took it, for a tool that closes
103
+ * itself: the tool's element leaves the DOM on close, which drops focus to the
104
+ * document body. `capture` runs as the tool takes focus and `restore` as it
105
+ * closes, before its element goes.
106
+ */
107
+ export function createFocusReturn() {
108
+ let opener = null;
109
+ return {
110
+ capture(container) {
111
+ if (typeof document === "undefined")
112
+ return;
113
+ const active = getDeepActiveElement(document);
114
+ if (active && container.contains(active))
115
+ return;
116
+ opener =
117
+ active instanceof HTMLElement && active !== document.body
118
+ ? active
119
+ : null;
120
+ },
121
+ restore() {
122
+ const target = opener;
123
+ opener = null;
124
+ if (!target?.isConnected)
125
+ return;
126
+ try {
127
+ target.focus();
128
+ }
129
+ catch {
130
+ // ignore
131
+ }
132
+ },
133
+ };
134
+ }
@@ -17,6 +17,11 @@ export interface Point {
17
17
  x: number;
18
18
  y: number;
19
19
  }
20
+ /** A box on screen: its top-left corner and its extent, as a `DOMRect` gives them. */
21
+ export interface ScreenBox extends Size {
22
+ left: number;
23
+ top: number;
24
+ }
20
25
  /**
21
26
  * The box `element` resolves its `left`/`top` against: its containing block, or
22
27
  * the viewport when that is the initial containing block.
@@ -77,3 +82,11 @@ export declare function uprightDockCentre(box: Size, strip: Size, degrees: numbe
77
82
  * where a box exceeds its block and centres on the offending axis.
78
83
  */
79
84
  export declare function clampPointWithinBlock(point: Point, box: Size, block: Size, gutter?: number): Point;
85
+ /**
86
+ * The top-left corner, in viewport coordinates, of a control strip docked
87
+ * beside an upright `tool`: centred below it, or above it when only the space
88
+ * above holds the strip, and slid along the tool's edge to stay inside the
89
+ * block. A strip that fits on neither side takes the roomier one; a strip wider
90
+ * than the block centres on it.
91
+ */
92
+ export declare function uprightDockCorner(tool: ScreenBox, strip: Size, block: ScreenBox, gap: number, gutter?: number): Point;
@@ -126,3 +126,26 @@ export function clampPointWithinBlock(point, box, block, gutter = DEFAULT_CONTAI
126
126
  const offset = clampOffsetWithinBlock({ x: point.x - centre.x, y: point.y - centre.y }, box, block, gutter);
127
127
  return { x: centre.x + offset.x, y: centre.y + offset.y };
128
128
  }
129
+ /**
130
+ * The top-left corner, in viewport coordinates, of a control strip docked
131
+ * beside an upright `tool`: centred below it, or above it when only the space
132
+ * above holds the strip, and slid along the tool's edge to stay inside the
133
+ * block. A strip that fits on neither side takes the roomier one; a strip wider
134
+ * than the block centres on it.
135
+ */
136
+ export function uprightDockCorner(tool, strip, block, gap, gutter = DEFAULT_CONTAINMENT_GUTTER) {
137
+ const top = block.top + gutter;
138
+ const bottom = block.top + block.height - gutter;
139
+ const below = tool.top + tool.height + gap;
140
+ const above = tool.top - gap - strip.height;
141
+ const roomBelow = bottom - below;
142
+ const roomAbove = tool.top - gap - top;
143
+ const y = roomBelow >= strip.height || (roomAbove < strip.height && roomBelow >= roomAbove)
144
+ ? below
145
+ : above;
146
+ const travel = block.width - gutter * 2 - strip.width;
147
+ const x = travel <= 0
148
+ ? block.left + (block.width - strip.width) / 2
149
+ : Math.max(block.left + gutter, Math.min(block.left + gutter + travel, tool.left + (tool.width - strip.width) / 2));
150
+ return { x, y };
151
+ }
@@ -53,6 +53,34 @@ export type OverlayPlacementNudge = keyof typeof OVERLAY_PLACEMENT_NUDGES;
53
53
  export type OverlayPlacementAnnouncement = (typeof OVERLAY_PLACEMENT_NUDGES)[OverlayPlacementNudge]["announceKey"] | "toolkit.announce.rotatedTo";
54
54
  /** The tap controls' rotations, the keyboard's two steps each way. */
55
55
  export declare const OVERLAY_PLACEMENT_ROTATIONS: readonly [number, number, 1, 5];
56
+ /**
57
+ * What the tap controls drive: a step for each arrow, and a turn for a tool
58
+ * that rotates. An `OverlayPlacementController` is one; an upright tool that
59
+ * keeps its own position supplies `nudge` alone.
60
+ */
61
+ export interface OverlayTapTarget {
62
+ nudge: (direction: OverlayPlacementNudge) => void;
63
+ rotateBy?: (degrees: number) => void;
64
+ }
65
+ /** A shrink-and-grow pair on the tap controls, labelled by the tool. */
66
+ export interface OverlaySizeControl {
67
+ shrink: {
68
+ glyph: string;
69
+ label: string;
70
+ };
71
+ grow: {
72
+ glyph: string;
73
+ label: string;
74
+ };
75
+ step: (direction: 1 | -1) => void;
76
+ }
77
+ /**
78
+ * Docks an upright tool's tap controls beside it, as `uprightDockCorner`
79
+ * places them. `OverlayPlacementControls` lays the controls out at the tool's
80
+ * top-left corner, so the offset is taken from there. A strip wider than the
81
+ * block wraps onto more rows, so a phone-width pane does not clip it.
82
+ */
83
+ export declare function dockUprightControls(element: HTMLElement, controls: HTMLElement): void;
56
84
  export interface OverlayPlacementOptions {
57
85
  /** The tool's positioned element, which receives the transform. */
58
86
  getElement: () => HTMLElement | undefined;
@@ -4,7 +4,7 @@
4
4
  * its `style.transform`, so a drag, an arrow key and a tap control always agree
5
5
  * about where the tool is.
6
6
  */
7
- import { clampOffsetOverlappingBlock, resolveContainingBlockRect, rotatedExtent, rotationCentreShift, uprightDockCentre, } from "./overlay-containment.js";
7
+ import { clampOffsetOverlappingBlock, DEFAULT_CONTAINMENT_GUTTER, resolveContainingBlockRect, rotatedExtent, rotationCentreShift, uprightDockCentre, uprightDockCorner, } from "./overlay-containment.js";
8
8
  import { createPointerDragController } from "./pointer-drag.js";
9
9
  import { createPointerGesture } from "./pointer-gesture.js";
10
10
  import { createPointerRotateController } from "./pointer-rotate.js";
@@ -60,8 +60,23 @@ const ARROW_KEYS = {
60
60
  };
61
61
  /** Pixels of the tool kept inside its containing block on each axis. */
62
62
  const MIN_VISIBLE = 100;
63
- /** Space between the tool's bottom edge and its tap controls. */
63
+ /** Space between the tool and its tap controls. */
64
64
  const CONTROLS_GAP = 8;
65
+ /**
66
+ * Docks an upright tool's tap controls beside it, as `uprightDockCorner`
67
+ * places them. `OverlayPlacementControls` lays the controls out at the tool's
68
+ * top-left corner, so the offset is taken from there. A strip wider than the
69
+ * block wraps onto more rows, so a phone-width pane does not clip it.
70
+ */
71
+ export function dockUprightControls(element, controls) {
72
+ const block = resolveContainingBlockRect(element);
73
+ if (!block)
74
+ return;
75
+ controls.style.maxWidth = `${Math.max(0, block.width - DEFAULT_CONTAINMENT_GUTTER * 2)}px`;
76
+ const tool = element.getBoundingClientRect();
77
+ const corner = uprightDockCorner(tool, { width: controls.offsetWidth, height: controls.offsetHeight }, block, CONTROLS_GAP);
78
+ controls.style.transform = `translate(${corner.x - tool.left - element.clientLeft}px, ${corner.y - tool.top - element.clientTop}px)`;
79
+ }
65
80
  export function createOverlayPlacement(options) {
66
81
  let placement = { x: 0, y: 0, rotation: 0 };
67
82
  const sizeOf = (element) => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-players/pie-players-shared",
3
- "version": "1.0.0-beta.3",
3
+ "version": "1.0.0-beta.4",
4
4
  "type": "module",
5
5
  "description": "Shared runtime + UI utilities for PIE players",
6
6
  "license": "MIT",
@@ -106,9 +106,9 @@
106
106
  "dist"
107
107
  ],
108
108
  "dependencies": {
109
- "@pie-element/shared-utils": "0.1.3",
109
+ "@pie-element/shared-utils": "0.1.4",
110
110
  "@pie-lib/math-rendering-module": "6.0.0",
111
- "dompurify": "^3.4.16",
111
+ "dompurify": "^3.4.17",
112
112
  "semver": "^7.8.5"
113
113
  },
114
114
  "scripts": {
@@ -125,7 +125,7 @@
125
125
  "@biomejs/biome": "^2.5.15",
126
126
  "@happy-dom/global-registrator": "^20.14.5",
127
127
  "@pie-element/shared-math-rendering-mathjax": "0.1.4",
128
- "@playwright/test": "^1.64.0",
128
+ "@playwright/test": "^1.64.1",
129
129
  "@types/semver": "^7.8.0",
130
130
  "es-module-shims": "^2.8.4",
131
131
  "esbuild": "^0.28.2",