@glissade/react 0.10.1 → 0.11.0-pre.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/dist/index.d.ts CHANGED
@@ -1,13 +1,50 @@
1
- import { ReadonlySignal, Signal } from "@glissade/core";
2
- import { Player } from "@glissade/player";
1
+ import { CSSProperties } from "react";
2
+ import { ReadonlySignal, Signal, Timeline } from "@glissade/core";
3
+ import { LoopMode, Player } from "@glissade/player";
4
+ import { Scene } from "@glissade/scene";
3
5
 
4
- //#region src/index.d.ts
6
+ //#region src/ScenePlayer.d.ts
5
7
 
8
+ interface ScenePlayerProps {
9
+ /** The scene graph to render. */
10
+ scene: Scene;
11
+ /** The timeline document driving it. */
12
+ timeline: Timeline;
13
+ /** Loop policy (forwarded to the Player). */
14
+ loop?: LoopMode;
15
+ /** Render the default controls bar (play/pause, scrubber, time readout). */
16
+ controls?: boolean;
17
+ /** Start playing on mount. */
18
+ autoplay?: boolean;
19
+ /** Fired when a play completes naturally (true) or is interrupted (false). */
20
+ onFinished?: (completed: boolean) => void;
21
+ /** Called once the underlying Player is mounted (for imperative control). */
22
+ onReady?: (player: Player) => void;
23
+ className?: string;
24
+ style?: CSSProperties;
25
+ }
26
+ declare function ScenePlayer({
27
+ scene,
28
+ timeline,
29
+ loop,
30
+ controls,
31
+ autoplay,
32
+ onFinished,
33
+ onReady,
34
+ className,
35
+ style
36
+ }: ScenePlayerProps): React.ReactElement;
37
+ //#endregion
38
+ //#region src/index.d.ts
6
39
  /** Subscribe a component to a signal; re-renders only when the value actually changes. */
7
40
  declare function useSignalValue<T>(sig: ReadonlySignal<T>): T;
41
+ /** DESIGN §4 sketch name — alias of {@link useSignalValue} (`useSignal(node.width)`). */
42
+ declare const useSignal: typeof useSignalValue;
8
43
  /** The player's current time, live. */
9
44
  declare function usePlayhead(player: Player): number;
10
- /** Playing state, derived from playhead motion + player state polling on invalidation. */
45
+ /** Playing state, live: `time` tracks the playhead, `playing` tracks the
46
+ * reactive `playingSignal` (so the value updates on a pause, which doesn't move
47
+ * the playhead). */
11
48
  declare function usePlayerState(player: Player): {
12
49
  playing: boolean;
13
50
  time: number;
@@ -25,5 +62,7 @@ declare function useMachineState(machine: {
25
62
  declare function useInput<T extends boolean | number>(machine: {
26
63
  input(name: string): Signal<T>;
27
64
  }, name: string): [T, (value: T) => void];
65
+ /** The declarative <ScenePlayer> over mount() (DESIGN §4.3, the <gs-player> twin). */
66
+
28
67
  //#endregion
29
- export { useInput, useMachineState, usePlayerState, usePlayhead, useSignalValue };
68
+ export { ScenePlayer, type ScenePlayerProps, useInput, useMachineState, usePlayerState, usePlayhead, useSignal, useSignalValue };
package/dist/index.js CHANGED
@@ -1,6 +1,113 @@
1
- import { useCallback, useSyncExternalStore } from "react";
1
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
2
2
  import "@glissade/core";
3
- import "@glissade/player";
3
+ import { mount } from "@glissade/player";
4
+ import "@glissade/scene";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/ScenePlayer.tsx
7
+ /**
8
+ * <ScenePlayer> (DESIGN.md §4.3 tier 3, the React twin of <gs-player>): a
9
+ * declarative component over mount(). Props are mount-native (`scene` +
10
+ * `timeline`); the controls inventory mirrors the element's — play/pause,
11
+ * scrubber, time readout — but as plain React with className-styled nodes
12
+ * rather than a shadow DOM + CSS parts.
13
+ *
14
+ * All mount() work happens in a useEffect keyed on [scene, timeline] (never
15
+ * during render — SSR renders the inert canvas only). The component owns
16
+ * playback so it can observe each play's `.finished` promise for onFinished
17
+ * (a per-play completion signal, §2 — NOT a polled state).
18
+ */
19
+ /**
20
+ * The controls bar — split out so it can subscribe to live playhead state via
21
+ * usePlayerState (a hook, so it must live in a component that only renders once
22
+ * the player exists).
23
+ */
24
+ function Controls({ player }) {
25
+ const { playing, time, duration } = usePlayerState(player);
26
+ return /* @__PURE__ */ jsxs("div", {
27
+ className: "gs-controls",
28
+ children: [
29
+ /* @__PURE__ */ jsx("button", {
30
+ className: "gs-controls-button",
31
+ type: "button",
32
+ "aria-label": "Play or pause",
33
+ onClick: () => {
34
+ if (player.playing) player.pause();
35
+ else player.play();
36
+ },
37
+ children: playing ? "Pause" : "Play"
38
+ }),
39
+ /* @__PURE__ */ jsx("input", {
40
+ className: "gs-controls-scrubber",
41
+ type: "range",
42
+ min: 0,
43
+ max: 1,
44
+ step: 1e-4,
45
+ value: duration > 0 ? time / duration : 0,
46
+ "aria-label": "Seek",
47
+ onChange: (e) => {
48
+ player.pause();
49
+ player.seek(parseFloat(e.target.value) * player.duration);
50
+ }
51
+ }),
52
+ /* @__PURE__ */ jsxs("span", {
53
+ className: "gs-controls-time",
54
+ children: [
55
+ time.toFixed(2),
56
+ " / ",
57
+ duration.toFixed(2),
58
+ "s"
59
+ ]
60
+ })
61
+ ]
62
+ });
63
+ }
64
+ function ScenePlayer({ scene, timeline, loop, controls = false, autoplay = false, onFinished, onReady, className, style }) {
65
+ const canvasRef = useRef(null);
66
+ const [player, setPlayer] = useState(null);
67
+ const onFinishedRef = useRef(onFinished);
68
+ onFinishedRef.current = onFinished;
69
+ const onReadyRef = useRef(onReady);
70
+ onReadyRef.current = onReady;
71
+ const autoplayRef = useRef(autoplay);
72
+ autoplayRef.current = autoplay;
73
+ useEffect(() => {
74
+ const canvas = canvasRef.current;
75
+ if (!canvas) return;
76
+ let cancelled = false;
77
+ const mounted = mount(scene, timeline, canvas, {
78
+ ...loop !== void 0 ? { loop } : {},
79
+ autoplay: false
80
+ });
81
+ const p = mounted.player;
82
+ const rawPlay = p.play.bind(p);
83
+ const playAndTrack = (opts) => {
84
+ const handle = rawPlay(opts);
85
+ handle.finished.then((completed) => {
86
+ if (!cancelled) onFinishedRef.current?.(completed);
87
+ }, () => void 0);
88
+ return handle;
89
+ };
90
+ p.play = playAndTrack;
91
+ setPlayer(p);
92
+ onReadyRef.current?.(p);
93
+ if (autoplayRef.current) playAndTrack();
94
+ return () => {
95
+ cancelled = true;
96
+ setPlayer(null);
97
+ mounted.dispose();
98
+ };
99
+ }, [scene, timeline]);
100
+ return /* @__PURE__ */ jsxs("div", {
101
+ className,
102
+ style,
103
+ children: [/* @__PURE__ */ jsx("canvas", {
104
+ ref: canvasRef,
105
+ width: scene.size.w,
106
+ height: scene.size.h
107
+ }), controls && player ? /* @__PURE__ */ jsx(Controls, { player }) : null]
108
+ });
109
+ }
110
+ //#endregion
4
111
  //#region src/index.ts
5
112
  /**
6
113
  * @glissade/react — thin adapters (DESIGN.md §4.3 tier 3): vanilla core,
@@ -13,15 +120,19 @@ function useSignalValue(sig) {
13
120
  const get = useCallback(() => sig.peek(), [sig]);
14
121
  return useSyncExternalStore(subscribe, get, get);
15
122
  }
123
+ /** DESIGN §4 sketch name — alias of {@link useSignalValue} (`useSignal(node.width)`). */
124
+ const useSignal = useSignalValue;
16
125
  /** The player's current time, live. */
17
126
  function usePlayhead(player) {
18
127
  return useSignalValue(player.playhead);
19
128
  }
20
- /** Playing state, derived from playhead motion + player state polling on invalidation. */
129
+ /** Playing state, live: `time` tracks the playhead, `playing` tracks the
130
+ * reactive `playingSignal` (so the value updates on a pause, which doesn't move
131
+ * the playhead). */
21
132
  function usePlayerState(player) {
22
133
  const time = usePlayhead(player);
23
134
  return {
24
- playing: player.playing,
135
+ playing: useSignalValue(player.playingSignal),
25
136
  time,
26
137
  duration: player.duration
27
138
  };
@@ -40,4 +151,4 @@ function useInput(machine, name) {
40
151
  return [useSignalValue(sig), useCallback((v) => sig.set(v), [sig])];
41
152
  }
42
153
  //#endregion
43
- export { useInput, useMachineState, usePlayerState, usePlayhead, useSignalValue };
154
+ export { ScenePlayer, useInput, useMachineState, usePlayerState, usePlayhead, useSignal, useSignalValue };
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@glissade/react",
3
- "version": "0.10.1",
3
+ "version": "0.11.0-pre.1",
4
4
  "description": "glissade React bindings: signals via useSyncExternalStore, player hooks.",
5
5
  "license": "Apache-2.0",
6
+ "engines": {
7
+ "node": ">=20.19"
8
+ },
6
9
  "type": "module",
7
10
  "sideEffects": false,
8
11
  "exports": {
@@ -15,15 +18,21 @@
15
18
  "dist"
16
19
  ],
17
20
  "dependencies": {
18
- "@glissade/core": "0.10.1",
19
- "@glissade/player": "0.10.1"
21
+ "@glissade/core": "0.11.0-pre.1",
22
+ "@glissade/player": "0.11.0-pre.1",
23
+ "@glissade/scene": "0.11.0-pre.1"
20
24
  },
21
25
  "peerDependencies": {
22
26
  "react": ">=18"
23
27
  },
24
28
  "devDependencies": {
29
+ "@testing-library/jest-dom": "^6.6.3",
30
+ "@testing-library/react": "^16.3.0",
25
31
  "@types/react": "^19.1.0",
26
- "react": "^19.1.0"
32
+ "@types/react-dom": "^19.1.0",
33
+ "jsdom": "^26.0.0",
34
+ "react": "^19.1.0",
35
+ "react-dom": "^19.1.0"
27
36
  },
28
37
  "repository": {
29
38
  "type": "git",