@simon-says-build/demo-player 0.8.0 → 0.8.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simon-says-build/demo-player",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
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
@@ -14,7 +14,8 @@
14
14
  // No JSX, so it needs nothing from the app's Babel setup beyond React Native's.
15
15
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
16
16
  import { Linking, Pressable, ScrollView, StyleSheet, Text, TextInput, View, useWindowDimensions } from 'react-native';
17
- import { cardBox, currentDemo, demoConfig, findTarget, handlerFor, onDemo, phoneSteps, playDemo, releaseOf, reviewChangesOf, scalePoint } from './core.js';
17
+ import { pageName } from './page-name.js';
18
+ import { cardBox, currentDemo, demoConfig, findTarget, handlerFor, onDemo, phoneSteps, playDemo, releaseOf, reviewChangesOf, scalePoint, stepTitle } from './core.js';
18
19
  import { demoApi, tourOf } from './api.js';
19
20
  import { signInFirst, openInApp } from './signin.js';
20
21
  import { runEvent, fillStep } from './events.js';
@@ -78,6 +79,7 @@ function Player({ config, router, insets, onClose, children }) {
78
79
  const [signIn, setSignIn] = useState(null); // a `signin` step: null while it runs, else { ok, error }
79
80
  const [event, setEvent] = useState(null); // an `event` step: null while it runs, else { ok, error }
80
81
  const [vars, setVars] = useState({}); // per person: what their events set up ({walkId} in later steps)
82
+ const [flash, setFlash] = useState(null); // "Now navigating to the walk page", said before a goto opens it
81
83
 
82
84
  useEffect(() => {
83
85
  let live = true;
@@ -125,15 +127,27 @@ function Player({ config, router, insets, onClose, children }) {
125
127
  api.progress({ status: 'done' }).catch(() => {});
126
128
  }, [api]);
127
129
 
130
+ const advance = useCallback(() => {
131
+ if (index + 1 >= steps.length) { if (changes.length) setReviewing(true); else finish(); }
132
+ else setIndex(index + 1);
133
+ }, [steps, index, finish, changes]);
134
+ // SAID BEFORE IT GOES (2026-11-01, Michael): a goto flashes "Now navigating
135
+ // to the walk page" for a moment, then opens it (schemas/page-name.mjs).
128
136
  const next = useCallback(() => {
129
- if (!steps) return;
137
+ if (!steps || flash) return;
130
138
  if (step?.kind === 'goto' && step.path) {
131
- if (step.path.startsWith('/') && router?.push) router.push(step.path);
132
- else Linking.openURL(step.path).catch(() => {});
139
+ const path = step.path;
140
+ setFlash(`Now navigating to ${pageName(path)}`);
141
+ setTimeout(() => {
142
+ if (path.startsWith('/') && router?.push) router.push(path);
143
+ else Linking.openURL(path).catch(() => {});
144
+ setFlash(null);
145
+ advance();
146
+ }, FLASH_MS);
147
+ return;
133
148
  }
134
- if (index + 1 >= steps.length) { if (changes.length) setReviewing(true); else finish(); }
135
- else setIndex(index + 1);
136
- }, [steps, step, index, router, finish, changes]);
149
+ advance();
150
+ }, [steps, step, router, advance, flash]);
137
151
 
138
152
  const doIt = useCallback(() => {
139
153
  if (!step || !found) return;
@@ -183,6 +197,8 @@ function Player({ config, router, insets, onClose, children }) {
183
197
  if (error) return h(Card, { at: null, screen, insets, card, setCard, title: 'Demo', body: `The demo didn't load: ${error}` });
184
198
  if (!steps) return null;
185
199
  if (reviewing && !tucked) return h(Review, { api, changes, release, screen, insets, onBack: steps.length ? () => setReviewing(false) : null, onDone: finish, onHide: () => setTucked(true) });
200
+ if (flash) return h(View, { pointerEvents: 'none', style: [s.flash, { top: insets.top + screen.height * 0.4 }], testID: 'ssb-demo-flash' },
201
+ h(Text, { style: s.flashText, accessibilityLiveRegion: 'polite' }, flash));
186
202
  if (tucked || done) {
187
203
  // A demo started at run time closes when it is done (▶ Demo again replays it);
188
204
  // one baked into a preview stays, to be replayed from its pill.
@@ -212,7 +228,7 @@ function Player({ config, router, insets, onClose, children }) {
212
228
  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,
213
229
  h(Card, {
214
230
  at: rect, place: step.place, screen, insets, card, setCard,
215
- title: `${index + 1} of ${steps.length}`, body: step.caption,
231
+ title: stepTitle(step, index, steps.length), body: step.caption,
216
232
  actions: [
217
233
  { label: 'Hide', onPress: () => setTucked(true) },
218
234
  index > 0 && steps[index - 1].kind !== 'signin' ? { label: 'Back', onPress: () => setIndex(index - 1) } : null,
@@ -307,6 +323,8 @@ function Card({ at, place, screen, insets, card, setCard, title, body, actions =
307
323
  }, h(Text, { style: [s.buttonText, a.primary && s.primaryText] }, a.label)))) : null);
308
324
  }
309
325
 
326
+ const FLASH_MS = 1100; // long enough to read "Now navigating to the walk page"
327
+
310
328
  const s = StyleSheet.create({
311
329
  root: { flex: 1 },
312
330
  ring: { position: 'absolute', borderRadius: 12, borderWidth: 3, borderColor: '#FFFFFF', shadowColor: '#000', shadowOpacity: 0.6, shadowRadius: 8, shadowOffset: { width: 0, height: 0 } },
@@ -327,6 +345,8 @@ const s = StyleSheet.create({
327
345
  changeState: { color: '#AEAEB2', fontSize: 13, marginTop: 4 },
328
346
  error: { color: '#FF6961', fontSize: 13, marginTop: 4 },
329
347
  input: { marginTop: 10, minHeight: 64, padding: 10, borderRadius: 10, backgroundColor: '#2C2C2E', color: '#FFFFFF', fontSize: 15 },
348
+ flash: { position: 'absolute', left: 32, right: 32, alignItems: 'center', paddingVertical: 14, paddingHorizontal: 18, borderRadius: 14, backgroundColor: 'rgba(28,28,30,0.92)' },
349
+ flashText: { color: '#FFFFFF', fontSize: 16, fontWeight: '600', textAlign: 'center' },
330
350
  pill: { position: 'absolute', right: 16, paddingVertical: 8, paddingHorizontal: 14, borderRadius: 18, backgroundColor: '#1C1C1E' },
331
351
  pillText: { color: '#FFFFFF', fontSize: 13, fontWeight: '600' },
332
352
  });
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.8.0';
13
+ export const PLAYER_VERSION = '0.8.2';
14
14
 
15
15
  export const HOST = 5;
16
16
  export const TEXT = 6;
@@ -103,6 +103,11 @@ export function scalePoint(point, screen, from = RECORDED_SCREEN) {
103
103
  };
104
104
  }
105
105
 
106
+ /** A step card's title: where it is in the demo, and whether SSB only set it up (a best-effort step: the state seeded and the screen opened, the change not driven). */
107
+ export function stepTitle(step, index, total) {
108
+ return `${index + 1} of ${total}${step?.bestEffort === true ? ' · set up by SSB' : ''}`;
109
+ }
110
+
106
111
  /** The steps of a tour a phone can play, in order. A step with no caption says nothing and is dropped. */
107
112
  export function phoneSteps(steps) {
108
113
  return (steps || []).filter((s) => s && PHONE_KINDS.has(s.kind) && String(s.caption || '').trim());
@@ -0,0 +1,23 @@
1
+ // THE PAGE A STEP OPENS, in words (2026-11-01, Michael: "before any navigation,
2
+ // can we show a brief flash showing 'Now navigating to [the X page]'"). Both
3
+ // players say it before they navigate: the phone's (packages/demo-player/src/
4
+ // page-name.js) and the extension's (extension/overlay.js) carry copies of
5
+ // pageName, held equal by bin/page-name.test.mjs. Pure.
6
+ //
7
+ // /walk/{walkId} → the walk page (an id or {placeholder} is not a name)
8
+ // /(app)/settings/profile → the profile page (an Expo Router group is not a name)
9
+ // /MainStack/Tabs/Dashboard → the dashboard page (a React Navigation chain)
10
+ // walk2gether://walk/abc → the walk page (a deep link)
11
+ // / → the home page
12
+ export function pageName(path) {
13
+ // A web address drops its host; a deep link's "host" is its first screen.
14
+ const rest = String(path || '').replace(/^https?:\/\/[^/]*/i, '').replace(/^[a-z][a-z0-9+.-]*:\/\//i, '/').split(/[?#]/)[0];
15
+ const isId = (s) => /^\{.*\}$/.test(s) || /^\[.*\]$/.test(s) || /^:/.test(s) || /^\(.*\)$/.test(s) || s === 'index'
16
+ || /^[0-9]+$/.test(s) || /^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(s) || (s.length >= 16 && /[0-9]/.test(s) && /^[A-Za-z0-9_-]+$/.test(s));
17
+ const parts = rest.split('/').filter(Boolean).map((s) => { try { return decodeURIComponent(s); } catch { return s; } }).filter((s) => !isId(s));
18
+ const last = parts[parts.length - 1];
19
+ if (!last) return 'the home page';
20
+ const words = last.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/[-_.]+/g, ' ').trim().toLowerCase();
21
+ return `the ${words || 'home'} page`;
22
+ }
23
+ // END pageName