@simon-says-build/demo-player 0.1.0 → 0.2.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/index.d.ts +9 -0
- package/index.js +1 -1
- package/package.json +13 -3
- package/src/DemoHost.js +13 -5
- package/src/api.js +4 -2
- package/src/core.js +17 -2
package/index.d.ts
CHANGED
|
@@ -15,3 +15,12 @@ export function DemoHost(props: {
|
|
|
15
15
|
insets?: { top?: number; bottom?: number };
|
|
16
16
|
children?: ReactNode;
|
|
17
17
|
}): ReactNode;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Play a demo now, in the bundle already running (the in-app SSB overlay's
|
|
21
|
+
* ▶ Demo hands SimonSays' answer here). `null` stops it. A demo baked into
|
|
22
|
+
* the bundle (`<DemoHost demo>`) outranks it. Returns false for an incomplete demo.
|
|
23
|
+
*/
|
|
24
|
+
export function playDemo(demo: DemoBundle | null): boolean;
|
|
25
|
+
export function currentDemo(): { id: string; token: string; api: string } | null;
|
|
26
|
+
export function onDemo(fn: (demo: { id: string; token: string; api: string } | null) => void): () => void;
|
package/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { DemoHost } from './src/DemoHost.js';
|
|
2
|
-
export { demoConfig, findTarget, handlerFor, phoneSteps, scalePoint } from './src/core.js';
|
|
2
|
+
export { demoConfig, findTarget, handlerFor, phoneSteps, scalePoint, playDemo, currentDemo, onDemo } from './src/core.js';
|
package/package.json
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simon-says-build/demo-player",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "SimonSays mobile demo player: walks a preview's viewer through a job's change, inside the app. Renders nothing unless the bundle carries a demo.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/simon-says-studio/simon-says-build.git",
|
|
8
|
+
"directory": "packages/demo-player"
|
|
9
|
+
},
|
|
5
10
|
"type": "module",
|
|
6
11
|
"main": "index.js",
|
|
7
12
|
"types": "index.d.ts",
|
|
8
|
-
"files": [
|
|
13
|
+
"files": [
|
|
14
|
+
"index.js",
|
|
15
|
+
"index.d.ts",
|
|
16
|
+
"src/",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
9
19
|
"peerDependencies": {
|
|
10
20
|
"react": ">=18",
|
|
11
21
|
"react-native": ">=0.76"
|
|
12
22
|
},
|
|
13
23
|
"scripts": {
|
|
14
|
-
"test": "node --test test
|
|
24
|
+
"test": "node --test test/*.test.mjs"
|
|
15
25
|
},
|
|
16
26
|
"license": "UNLICENSED"
|
|
17
27
|
}
|
package/src/DemoHost.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// No JSX, so it needs nothing from the app's Babel setup beyond React Native's.
|
|
10
10
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
11
11
|
import { Linking, Pressable, StyleSheet, Text, View, useWindowDimensions } from 'react-native';
|
|
12
|
-
import { cardBox, demoConfig, findTarget, handlerFor, phoneSteps, scalePoint } from './core.js';
|
|
12
|
+
import { cardBox, currentDemo, demoConfig, findTarget, handlerFor, onDemo, phoneSteps, playDemo, scalePoint } from './core.js';
|
|
13
13
|
import { demoApi, tourOf } from './api.js';
|
|
14
14
|
|
|
15
15
|
const h = React.createElement;
|
|
@@ -44,12 +44,16 @@ const measure = (fiber) => new Promise((resolve) => {
|
|
|
44
44
|
const DEFAULT_INSETS = { top: 60, bottom: 100 };
|
|
45
45
|
|
|
46
46
|
export function DemoHost({ demo, router, insets, children }) {
|
|
47
|
-
const
|
|
47
|
+
const baked = useMemo(() => demoConfig(demo || {}), [demo?.id, demo?.token, demo?.api]);
|
|
48
|
+
const [started, setStarted] = useState(currentDemo);
|
|
49
|
+
useEffect(() => onDemo(setStarted), []);
|
|
50
|
+
const config = baked || started;
|
|
48
51
|
if (!config) return children ?? null;
|
|
49
|
-
|
|
52
|
+
// Keyed by the demo: a second ▶ Demo starts its own tour from the first step.
|
|
53
|
+
return h(Player, { key: config.id, config, router, insets: { ...DEFAULT_INSETS, ...(insets || {}) }, onClose: baked ? null : () => playDemo(null) }, children);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
function Player({ config, router, insets, children }) {
|
|
56
|
+
function Player({ config, router, insets, onClose, children }) {
|
|
53
57
|
const rootRef = useRef(null);
|
|
54
58
|
const screen = useWindowDimensions();
|
|
55
59
|
const api = useMemo(() => demoApi(config), [config]);
|
|
@@ -65,7 +69,7 @@ function Player({ config, router, insets, children }) {
|
|
|
65
69
|
useEffect(() => {
|
|
66
70
|
let live = true;
|
|
67
71
|
api.load()
|
|
68
|
-
.then((d) => { if (live) setSteps(phoneSteps(tourOf(d)).concat()); })
|
|
72
|
+
.then((d) => { if (live) setSteps(phoneSteps(tourOf(d, { here: !!config.here })).concat()); })
|
|
69
73
|
.catch((e) => { if (live) setError(e.message); });
|
|
70
74
|
return () => { live = false; };
|
|
71
75
|
}, [api]);
|
|
@@ -127,6 +131,10 @@ function Player({ config, router, insets, children }) {
|
|
|
127
131
|
if (error) return h(Card, { at: null, screen, insets, card, setCard, title: 'Demo', body: `The demo didn't load: ${error}` });
|
|
128
132
|
if (!steps) return null;
|
|
129
133
|
if (tucked || done) {
|
|
134
|
+
// A demo started at run time closes when it is done (▶ Demo again replays it);
|
|
135
|
+
// one baked into a preview stays, to be replayed from its pill.
|
|
136
|
+
if (done && onClose) return h(Pressable, { style: [s.pill, { bottom: insets.bottom + 12 }], onPress: onClose, accessibilityRole: 'button', accessibilityLabel: 'Close the demo', testID: 'ssb-demo-close' },
|
|
137
|
+
h(Text, { style: s.pillText }, 'Demo done · close'));
|
|
130
138
|
return h(Pressable, { style: [s.pill, { bottom: insets.bottom + 12 }], onPress: () => { setTucked(false); if (done) { setDone(false); setIndex(0); } }, accessibilityRole: 'button', accessibilityLabel: 'Open the demo' },
|
|
131
139
|
h(Text, { style: s.pillText }, done ? 'Demo done · replay' : `Demo ${index + 1}/${steps.length}`));
|
|
132
140
|
}
|
package/src/api.js
CHANGED
|
@@ -17,12 +17,14 @@ export function demoApi({ api, id, token }, fetchImpl = globalThis.fetch) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/** The tour a phone plays: every actor's steps, each actor introduced by who to be. */
|
|
20
|
-
|
|
20
|
+
// A demo played in the build the viewer is already using, signed in as
|
|
21
|
+
// themselves (`here`, the overlay's ▶ Demo), is not told who to sign in as.
|
|
22
|
+
export function tourOf(demo, { here = false } = {}) {
|
|
21
23
|
const out = [];
|
|
22
24
|
for (const [actorIndex, a] of (demo?.actors || []).entries()) {
|
|
23
25
|
const steps = a.tour?.steps || [];
|
|
24
26
|
if (!steps.length) continue;
|
|
25
|
-
out.push({ kind: 'say', caption: `This part is as ${a.label}. Sign in as ${a.email} if you aren't already, then tap Next.`, actorIndex, intro: true });
|
|
27
|
+
if (!here) out.push({ kind: 'say', caption: `This part is as ${a.label}. Sign in as ${a.email} if you aren't already, then tap Next.`, actorIndex, intro: true });
|
|
26
28
|
for (const s of steps) out.push({ ...s, actorIndex });
|
|
27
29
|
}
|
|
28
30
|
return out;
|
package/src/core.js
CHANGED
|
@@ -103,9 +103,9 @@ export function phoneSteps(steps) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/** The demo this bundle carries (EXPO_PUBLIC_SSB_DEMO_*), or null when it carries none. */
|
|
106
|
-
export function demoConfig({ id, token, api } = {}) {
|
|
106
|
+
export function demoConfig({ id, token, api, here } = {}) {
|
|
107
107
|
if (!id || !token || !api) return null;
|
|
108
|
-
return { id: String(id), token: String(token), api: String(api).replace(/\/+$/, '') };
|
|
108
|
+
return { id: String(id), token: String(token), api: String(api).replace(/\/+$/, ''), ...(here ? { here: true } : {}) };
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// The card's box, inside the band between the insets. With no element and no
|
|
@@ -118,3 +118,18 @@ export function cardBox(place, at, screen, insets, card) {
|
|
|
118
118
|
const box = bubbleBox(place, el, band, { w: card.w, h: card.h });
|
|
119
119
|
return { left: box.left, top: box.top + insets.top };
|
|
120
120
|
}
|
|
121
|
+
|
|
122
|
+
// A DEMO STARTED AT RUN TIME (2026-10-20, the in-app overlay's ▶ Demo, "Ready
|
|
123
|
+
// is this build"): the bundle running IS the preview of the work, so nothing
|
|
124
|
+
// is baked into it — SimonSays hands the overlay a demo's id and token, and
|
|
125
|
+
// the overlay hands them here. <DemoHost> plays a baked demo first, else this.
|
|
126
|
+
let runtime = null;
|
|
127
|
+
const listeners = new Set();
|
|
128
|
+
/** Play this demo now ({ id, token, api }); null stops it. */
|
|
129
|
+
export function playDemo(demo) {
|
|
130
|
+
runtime = demo ? demoConfig(demo) : null;
|
|
131
|
+
for (const fn of listeners) fn(runtime);
|
|
132
|
+
return !!runtime || !demo;
|
|
133
|
+
}
|
|
134
|
+
export const currentDemo = () => runtime;
|
|
135
|
+
export function onDemo(fn) { listeners.add(fn); return () => listeners.delete(fn); }
|