@simon-says-build/demo-player 0.12.0 → 0.13.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 +5 -0
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/DemoHost.js +58 -9
- package/src/core.js +30 -2
package/index.d.ts
CHANGED
|
@@ -45,3 +45,8 @@ export function navigationRoute(nav: unknown): string;
|
|
|
45
45
|
export function navigationRouter(nav: unknown): { push: (path: string) => boolean };
|
|
46
46
|
export function navigateArgs(path: string): [string] | [string, object] | null;
|
|
47
47
|
export function pathOfState(state: unknown): string;
|
|
48
|
+
|
|
49
|
+
/** While a review is collapsed (0.13.0): the function that resumes it, else null. */
|
|
50
|
+
export function tuckedDemo(): (() => void) | null;
|
|
51
|
+
/** The in-app SSB overlay says it is on screen, so a collapsed review leaves only its button. */
|
|
52
|
+
export function announceOverlay(): void;
|
package/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { DemoHost } from './src/DemoHost.js';
|
|
2
|
-
export { demoConfig, findTarget, handlerFor, phoneSteps, scalePoint, playDemo, currentDemo, onDemo, reviewChangesOf, releaseOf, releaseReviewsOf, PLAYER_VERSION } from './src/core.js';
|
|
2
|
+
export { demoConfig, findTarget, handlerFor, phoneSteps, scalePoint, playDemo, currentDemo, onDemo, reviewChangesOf, releaseOf, releaseReviewsOf, PLAYER_VERSION, tuckedDemo, announceOverlay } from './src/core.js';
|
|
3
3
|
export { navigationRoute, navigationRouter, navigateArgs, pathOfState } from './src/navigation.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simon-says-build/demo-player",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
package/src/DemoHost.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
16
16
|
import { Linking, NativeModules, Pressable, ScrollView, StyleSheet, Text, TextInput, View, useWindowDimensions } from 'react-native';
|
|
17
17
|
import { pageName } from './page-name.js';
|
|
18
|
-
import { OVERLAY_ID, cardBox, currentDemo, demoConfig, findTarget, handlerFor, introBody, introOf, onDemo, phoneSteps, playDemo, releaseOf, reviewChangesOf, scalePoint, rescueNote, screenCandidates, stepTitle, targetOfKey } from './core.js';
|
|
18
|
+
import { OVERLAY_ID, cardBox, pinnedBox, setTuckedDemo, overlayAnnounced, currentDemo, demoConfig, findTarget, handlerFor, introBody, introOf, onDemo, phoneSteps, playDemo, releaseOf, reviewChangesOf, scalePoint, rescueNote, screenCandidates, stepTitle, targetOfKey } from './core.js';
|
|
19
19
|
import { demoApi, tourOf } from './api.js';
|
|
20
20
|
import { signInFirst, openInApp } from './signin.js';
|
|
21
21
|
import { runEvent, fillStep } from './events.js';
|
|
@@ -162,6 +162,24 @@ function Player({ config, router, insets, onClose, children }) {
|
|
|
162
162
|
return () => { live = false; clearInterval(t); };
|
|
163
163
|
}, [step, done, screen.width, screen.height, rescue?.status, index]);
|
|
164
164
|
|
|
165
|
+
// TRY IT (0.13.0): the viewer taps the ringed element; once it was on
|
|
166
|
+
// screen and is gone (the app moved on), that was the tap — said, and the
|
|
167
|
+
// tour carries on. "Show me" presses it for them; Skip moves on.
|
|
168
|
+
const triedSeen = useRef(false);
|
|
169
|
+
const [tried, setTried] = useState(false);
|
|
170
|
+
useEffect(() => { triedSeen.current = false; setTried(false); }, [index]);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (step?.kind !== 'try' || tried) return;
|
|
173
|
+
if (found) { triedSeen.current = true; return; }
|
|
174
|
+
if (triedSeen.current) setTried(true);
|
|
175
|
+
}, [found, step?.kind, tried]);
|
|
176
|
+
|
|
177
|
+
// Collapsed, the SSB button resumes it (core.js tuckedDemo).
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
setTuckedDemo(tucked && !done ? () => setTucked(false) : null);
|
|
180
|
+
return () => setTuckedDemo(null);
|
|
181
|
+
}, [tucked, done]);
|
|
182
|
+
|
|
165
183
|
const finish = useCallback(() => {
|
|
166
184
|
setReviewing(false);
|
|
167
185
|
setDone(true);
|
|
@@ -190,8 +208,16 @@ function Player({ config, router, insets, onClose, children }) {
|
|
|
190
208
|
advance();
|
|
191
209
|
}, [steps, step, router, advance, flash]);
|
|
192
210
|
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (!tried) return undefined;
|
|
213
|
+
setFlash('✓ That\'s it');
|
|
214
|
+
const t = setTimeout(() => { setFlash(null); advance(); }, FLASH_MS);
|
|
215
|
+
return () => clearTimeout(t);
|
|
216
|
+
}, [tried]);
|
|
217
|
+
|
|
193
218
|
const doIt = useCallback(() => {
|
|
194
219
|
if (!step || !found) return;
|
|
220
|
+
if (step.kind === 'try') { handlerFor(found, 'onPress')?.({ nativeEvent: {}, preventDefault() {}, stopPropagation() {} }); return; }
|
|
195
221
|
if (step.kind === 'click') handlerFor(found, 'onPress')?.({ nativeEvent: {}, preventDefault() {}, stopPropagation() {} });
|
|
196
222
|
if (step.kind === 'fill' && step.value != null) handlerFor(found, 'onChangeText')?.(step.value);
|
|
197
223
|
next();
|
|
@@ -248,13 +274,14 @@ function Player({ config, router, insets, onClose, children }) {
|
|
|
248
274
|
if (!steps) return null;
|
|
249
275
|
if (intro && !tucked && !done) {
|
|
250
276
|
return h(Card, {
|
|
251
|
-
at: null, screen, insets, card, setCard, title: 'Guided Review', body: introBody(intro),
|
|
277
|
+
at: null, screen, insets, card, setCard, onCollapse: () => setTucked(true), title: 'Guided Review', body: introBody(intro),
|
|
252
278
|
actions: [{ label: 'Hide', onPress: () => setTucked(true) }, { label: 'Start the review', onPress: () => setIntro(null), primary: true }],
|
|
253
279
|
});
|
|
254
280
|
}
|
|
255
281
|
if (reviewing && !tucked) return h(Review, { api, changes, release, screen, insets, onBack: steps.length ? () => setReviewing(false) : null, onDone: finish, onHide: () => setTucked(true) });
|
|
256
282
|
if (flash) return h(View, { pointerEvents: 'none', style: [s.flash, { top: insets.top + screen.height * 0.4 }], testID: 'ssb-demo-flash' },
|
|
257
283
|
h(Text, { style: s.flashText, accessibilityLiveRegion: 'polite' }, flash));
|
|
284
|
+
if (tucked && !done && overlayAnnounced()) return null; // the SSB button resumes it (0.13.0)
|
|
258
285
|
if (tucked || done) {
|
|
259
286
|
// A demo started at run time closes when it is done (▶ Demo again replays it);
|
|
260
287
|
// one baked into a preview stays, to be replayed from its pill.
|
|
@@ -271,23 +298,35 @@ function Player({ config, router, insets, onClose, children }) {
|
|
|
271
298
|
// there is no Next: Try again, or Hide.
|
|
272
299
|
if (step.kind === 'event') {
|
|
273
300
|
return h(Card, {
|
|
274
|
-
at: null, screen, insets, card, setCard, title: `${index + 1} of ${steps.length}`,
|
|
301
|
+
at: null, screen, insets, card, setCard, onCollapse: () => setTucked(true), title: `${index + 1} of ${steps.length}`,
|
|
275
302
|
body: event ? `The review couldn't set this up, and the rest of it depends on it: ${event.error}` : step.caption,
|
|
276
303
|
actions: event ? [{ label: 'Hide', onPress: () => setTucked(true) }, { label: 'Try again', onPress: () => setEventTry((n) => n + 1), primary: true }] : [],
|
|
277
304
|
});
|
|
278
305
|
}
|
|
279
306
|
if (step.kind === 'signin') {
|
|
280
307
|
return h(Card, {
|
|
281
|
-
at: null, screen, insets, card, setCard, title: `${index + 1} of ${steps.length}`,
|
|
308
|
+
at: null, screen, insets, card, setCard, onCollapse: () => setTucked(true), title: `${index + 1} of ${steps.length}`,
|
|
282
309
|
body: signIn ? `SSB couldn't sign you in, and the review needs it: ${signIn.error}.` : step.caption,
|
|
283
310
|
actions: signIn ? [{ label: 'Hide', onPress: () => setTucked(true) }, { label: 'Try again', onPress: () => setSignInTry((n) => n + 1), primary: true }] : [],
|
|
284
311
|
});
|
|
285
312
|
}
|
|
313
|
+
if (step.kind === 'try') {
|
|
314
|
+
return h(React.Fragment, null,
|
|
315
|
+
rect ? h(View, { pointerEvents: 'none', style: [s.ring, s.tryRing, { left: rect.left - 6, top: rect.top - 6, width: rect.width + 12, height: rect.height + 12 }] }) : null,
|
|
316
|
+
h(Card, {
|
|
317
|
+
at: rect, screen, insets, card, setCard, onCollapse: () => setTucked(true),
|
|
318
|
+
title: `Try it · ${index + 1} of ${steps.length}`, body: rect ? step.caption : `${step.caption} (looking for it on this screen…)`,
|
|
319
|
+
actions: [
|
|
320
|
+
{ label: 'Skip', onPress: next },
|
|
321
|
+
found && handlerFor(found, 'onPress') ? { label: 'Show me', onPress: doIt, primary: true } : null,
|
|
322
|
+
].filter(Boolean),
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
286
325
|
const canDo = (step.kind === 'click' || step.kind === 'fill') && found && (step.kind === 'click' ? handlerFor(found, 'onPress') : handlerFor(found, 'onChangeText'));
|
|
287
326
|
return h(React.Fragment, null,
|
|
288
327
|
rect ? h(View, { pointerEvents: 'none', style: [s.ring, { left: rect.left - 6, top: rect.top - 6, width: rect.width + 12, height: rect.height + 12 }] }) : null,
|
|
289
328
|
h(Card, {
|
|
290
|
-
at: rect, place: step.place, screen, insets, card, setCard,
|
|
329
|
+
at: rect, place: step.place, screen, insets, card, setCard, onCollapse: () => setTucked(true),
|
|
291
330
|
title: stepTitle(step, index, steps.length), body: rescueNote(step.caption, rescue),
|
|
292
331
|
actions: [
|
|
293
332
|
{ label: 'Hide', onPress: () => setTucked(true) },
|
|
@@ -422,17 +461,23 @@ const button = (label, onPress, primary = false) => h(Pressable, {
|
|
|
422
461
|
style: ({ pressed }) => [s.button, primary && s.primary, pressed && s.pressed],
|
|
423
462
|
}, h(Text, { style: [s.buttonText, primary && s.primaryText] }, label));
|
|
424
463
|
|
|
425
|
-
|
|
426
|
-
|
|
464
|
+
// Pinned across the top (0.13.0, core.js pinnedBox), with a collapse control
|
|
465
|
+
// in its corner when the player can be collapsed (`onCollapse`).
|
|
466
|
+
function Card({ at, place, screen, insets, card, setCard, title, body, actions = [], onCollapse = null }) {
|
|
467
|
+
void place; void cardBox;
|
|
468
|
+
const box = pinnedBox(at, screen, insets, card);
|
|
427
469
|
return h(View, {
|
|
428
|
-
style: [s.card, { left: box.left, top: box.top,
|
|
470
|
+
style: [s.card, { left: box.left, top: box.top, width: box.width }],
|
|
429
471
|
onLayout: (e) => {
|
|
430
472
|
const { width, height } = e.nativeEvent.layout;
|
|
431
473
|
if (Math.abs(width - card.w) > 1 || Math.abs(height - card.h) > 1) setCard({ w: width, h: height });
|
|
432
474
|
},
|
|
433
475
|
accessibilityViewIsModal: false,
|
|
434
476
|
},
|
|
435
|
-
h(
|
|
477
|
+
h(View, { style: s.head },
|
|
478
|
+
h(Text, { style: [s.title, { flex: 1 }] }, title),
|
|
479
|
+
onCollapse ? h(Pressable, { testID: 'ssb-demo-collapse', onPress: onCollapse, hitSlop: 12, accessibilityRole: 'button', accessibilityLabel: 'Collapse the review',
|
|
480
|
+
style: ({ pressed }) => [s.collapse, pressed && s.pressed] }, h(Text, { style: s.collapseText }, '⌃')) : null),
|
|
436
481
|
h(Text, { style: s.body }, body),
|
|
437
482
|
actions.length ? h(View, { style: s.actions }, ...actions.map((a) => h(Pressable, {
|
|
438
483
|
key: a.label, onPress: a.onPress, accessibilityRole: 'button', accessibilityLabel: a.label,
|
|
@@ -448,6 +493,10 @@ const s = StyleSheet.create({
|
|
|
448
493
|
ring: { position: 'absolute', borderRadius: 12, borderWidth: 3, borderColor: '#FFFFFF', shadowColor: '#000', shadowOpacity: 0.6, shadowRadius: 8, shadowOffset: { width: 0, height: 0 } },
|
|
449
494
|
card: { position: 'absolute', width: 320, padding: 16, borderRadius: 16, backgroundColor: '#1C1C1E', shadowColor: '#000', shadowOpacity: 0.35, shadowRadius: 16, shadowOffset: { width: 0, height: 6 } },
|
|
450
495
|
title: { color: '#AEAEB2', fontSize: 12, fontWeight: '600', marginBottom: 6 },
|
|
496
|
+
head: { flexDirection: 'row', alignItems: 'flex-start' },
|
|
497
|
+
collapse: { marginLeft: 8, marginTop: -6, paddingHorizontal: 8, paddingVertical: 2, borderRadius: 8, backgroundColor: '#2C2C2E' },
|
|
498
|
+
collapseText: { color: '#FFFFFF', fontSize: 16, fontWeight: '700' },
|
|
499
|
+
tryRing: { borderColor: '#FFD60A', borderWidth: 4 },
|
|
451
500
|
body: { color: '#FFFFFF', fontSize: 16, lineHeight: 22 },
|
|
452
501
|
actions: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'flex-end', marginTop: 14, gap: 8 },
|
|
453
502
|
button: { paddingVertical: 8, paddingHorizontal: 12, borderRadius: 10, backgroundColor: '#2C2C2E' },
|
package/src/core.js
CHANGED
|
@@ -10,7 +10,7 @@ import { bubbleBox } from './step-place.js';
|
|
|
10
10
|
// What this player is, said when the app registers for "Ready to see"
|
|
11
11
|
// (packages/ssb-overlay sends it; functions/in-app-notify.mjs clientOf). Kept
|
|
12
12
|
// equal to package.json by test/core.test.mjs.
|
|
13
|
-
export const PLAYER_VERSION = '0.
|
|
13
|
+
export const PLAYER_VERSION = '0.13.0';
|
|
14
14
|
|
|
15
15
|
export const HOST = 5;
|
|
16
16
|
export const TEXT = 6;
|
|
@@ -21,7 +21,7 @@ export const RECORDED_SCREEN = { width: 393, height: 852 };
|
|
|
21
21
|
// What a phone can perform. `press` (a keyboard key), `select` (a web
|
|
22
22
|
// <select>) and `run` (a terminal demo) have no phone equivalent. `signin` is
|
|
23
23
|
// the player's own (tourOf, 0.6.0): sign this person in by link.
|
|
24
|
-
const PHONE_KINDS = new Set(['say', 'goto', 'show', 'click', 'fill', 'signin', 'event']);
|
|
24
|
+
const PHONE_KINDS = new Set(['say', 'goto', 'show', 'click', 'fill', 'signin', 'event', 'try']);
|
|
25
25
|
|
|
26
26
|
const lower = (s) => String(s ?? '').trim().toLowerCase();
|
|
27
27
|
const hiddenHost = (f) => {
|
|
@@ -59,6 +59,11 @@ const textOf = (f) => (typeof f.memoizedProps === 'string' ? f.memoizedProps : S
|
|
|
59
59
|
*/
|
|
60
60
|
export function findTarget(root, target) {
|
|
61
61
|
if (!target) return null;
|
|
62
|
+
// A LIST ITEM by the start of its testID (0.13.0): `walk-card-participant-`
|
|
63
|
+
// is the first walker's photo, whoever the copy's seed made them.
|
|
64
|
+
if (target.testIdPrefix) {
|
|
65
|
+
for (const f of walk(root)) if (f.tag === HOST && String(f.memoizedProps?.testID ?? '').startsWith(target.testIdPrefix)) return f;
|
|
66
|
+
}
|
|
62
67
|
if (target.testId) {
|
|
63
68
|
for (const f of walk(root)) if (f.tag === HOST && f.memoizedProps?.testID === target.testId) return f;
|
|
64
69
|
}
|
|
@@ -174,6 +179,29 @@ export function cardBox(place, at, screen, insets, card) {
|
|
|
174
179
|
return { left: box.left, top: box.top + insets.top };
|
|
175
180
|
}
|
|
176
181
|
|
|
182
|
+
// PINNED TO THE TOP (0.13.0, Michael 2026-11-05: "the demo should maybe be
|
|
183
|
+
// pinned to the top by default, but with a collapse button"). The card sits
|
|
184
|
+
// across the top of the screen; only when it would cover the ringed element
|
|
185
|
+
// (`at`) does it move to the bottom. Pure.
|
|
186
|
+
export function pinnedBox(at, screen, insets, card) {
|
|
187
|
+
const left = 12, width = screen.width - 24;
|
|
188
|
+
const top = insets.top + 8;
|
|
189
|
+
const bottom = screen.height - insets.bottom - card.h - 8;
|
|
190
|
+
const covers = at && at.top < top + card.h + 8 && at.bottom > top;
|
|
191
|
+
return { left, width, top: covers ? Math.max(top, bottom) : top };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// COLLAPSED, AND RESUMED FROM THE SSB BUTTON (0.13.0): collapsing leaves only
|
|
195
|
+
// the in-app SSB button (packages/ssb-overlay), whose tap resumes the review.
|
|
196
|
+
// The overlay says it is there (`announceOverlay`); without one the player
|
|
197
|
+
// keeps its own pill. `tuckedDemo()` is the resume function while collapsed.
|
|
198
|
+
let tuck = null;
|
|
199
|
+
let overlayHere = false;
|
|
200
|
+
export function setTuckedDemo(resume) { tuck = typeof resume === 'function' ? resume : null; }
|
|
201
|
+
export const tuckedDemo = () => tuck;
|
|
202
|
+
export function announceOverlay() { overlayHere = true; }
|
|
203
|
+
export const overlayAnnounced = () => overlayHere;
|
|
204
|
+
|
|
177
205
|
// A DEMO STARTED AT RUN TIME (2026-10-20, the in-app overlay's ▶ Demo, "Ready
|
|
178
206
|
// is this build"): the bundle running IS the preview of the work, so nothing
|
|
179
207
|
// is baked into it — SimonSays hands the overlay a demo's id and token, and
|