@simon-says-build/demo-player 0.9.0 → 0.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simon-says-build/demo-player",
3
- "version": "0.9.0",
3
+ "version": "0.10.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, 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, onDemo, phoneSteps, playDemo, releaseOf, reviewChangesOf, scalePoint, rescueNote, screenCandidates, stepTitle, targetOfKey } from './core.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';
19
19
  import { demoApi, tourOf } from './api.js';
20
20
  import { signInFirst, openInApp } from './signin.js';
21
21
  import { runEvent, fillStep } from './events.js';
@@ -76,6 +76,7 @@ function Player({ config, router, insets, onClose, children }) {
76
76
  const [changes, setChanges] = useState([]); // what the last card reviews (reviewChangesOf)
77
77
  const [release, setRelease] = useState(null); // a release's demo ends on releasing it (releaseOf)
78
78
  const [reviewing, setReviewing] = useState(false);
79
+ const [intro, setIntro] = useState(null); // the first screen: who asked, what, signed in as (introOf)
79
80
  const [signIn, setSignIn] = useState(null); // a `signin` step: null while it runs, else { ok, error }
80
81
  const [event, setEvent] = useState(null); // an `event` step: null while it runs, else { ok, error }
81
82
  const [vars, setVars] = useState({}); // per person: what their events set up ({walkId} in later steps)
@@ -86,7 +87,7 @@ function Player({ config, router, insets, onClose, children }) {
86
87
  useEffect(() => {
87
88
  let live = true;
88
89
  api.load()
89
- .then((d) => { if (live) { setSteps(phoneSteps(tourOf(d, { here: !!config.here })).concat()); setChanges(reviewChangesOf(d)); setRelease(releaseOf(d)); } })
90
+ .then((d) => { if (live) { setSteps(phoneSteps(tourOf(d, { here: !!config.here })).concat()); setChanges(reviewChangesOf(d)); setRelease(releaseOf(d)); setIntro(introOf(d)); } })
90
91
  .catch((e) => { if (live) setError(e.message); });
91
92
  return () => { live = false; };
92
93
  }, [api]);
@@ -221,6 +222,12 @@ function Player({ config, router, insets, onClose, children }) {
221
222
  const overlay = (() => {
222
223
  if (error) return h(Card, { at: null, screen, insets, card, setCard, title: 'Demo', body: `The demo didn't load: ${error}` });
223
224
  if (!steps) return null;
225
+ if (intro && !tucked && !done) {
226
+ return h(Card, {
227
+ at: null, screen, insets, card, setCard, title: 'Guided Review', body: introBody(intro),
228
+ actions: [{ label: 'Hide', onPress: () => setTucked(true) }, { label: 'Start the review', onPress: () => setIntro(null), primary: true }],
229
+ });
230
+ }
224
231
  if (reviewing && !tucked) return h(Review, { api, changes, release, screen, insets, onBack: steps.length ? () => setReviewing(false) : null, onDone: finish, onHide: () => setTucked(true) });
225
232
  if (flash) return h(View, { pointerEvents: 'none', style: [s.flash, { top: insets.top + screen.height * 0.4 }], testID: 'ssb-demo-flash' },
226
233
  h(Text, { style: s.flashText, accessibilityLiveRegion: 'polite' }, flash));
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.9.0';
13
+ export const PLAYER_VERSION = '0.10.0';
14
14
 
15
15
  export const HOST = 5;
16
16
  export const TEXT = 6;
@@ -219,3 +219,30 @@ export function releaseOf(demo) {
219
219
  if (!sm || typeof sm.jobId !== 'string' || !sm.jobId) return null;
220
220
  return { jobId: sm.jobId, label: String(sm.label || 'Approve & release'), note: String(sm.note || '') };
221
221
  }
222
+
223
+ // THE FIRST SCREEN OF A REVIEW (0.10.0, Michael 2026-11-03): a very brief
224
+ // outline before anything plays — who asked for it, what they asked for, and
225
+ // who the review signs in as — then a button to start. Pure.
226
+ // → { requestedBy: [string], asks: [{ title, asked }], as: [string] } | null
227
+ const firstSentence = (t) => { const x = String(t || '').replace(/\s+/g, ' ').trim(); const m = /^(.{20,160}?[.!?])(\s|$)/.exec(x); return (m ? m[1] : x.slice(0, 140) + (x.length > 140 ? '…' : '')); };
228
+ export function introOf(demo) {
229
+ const asks = [], by = new Set(), as = [];
230
+ for (const a of demo?.actors || []) {
231
+ if (a?.label && !as.includes(a.label)) as.push(String(a.label));
232
+ for (const sec of a.sections || []) for (const f of sec.features || []) {
233
+ if (f?.requestedBy) by.add(String(f.requestedBy).trim());
234
+ if (!asks.some((x) => x.title === f.title)) asks.push({ title: String(f.title || 'This change'), ...(f.tryIt?.steps ? { asked: firstSentence(f.tryIt.steps) } : {}) });
235
+ }
236
+ }
237
+ return asks.length || as.length ? { requestedBy: [...by], asks, as } : null;
238
+ }
239
+ export function introBody(intro) {
240
+ if (!intro) return '';
241
+ const lines = [];
242
+ if (intro.requestedBy.length) lines.push(`Requested by ${intro.requestedBy.join(', ')}`);
243
+ const shown = intro.asks.slice(0, 3);
244
+ for (const a of shown) lines.push(`• ${a.title}`);
245
+ if (intro.asks.length > shown.length) lines.push(`…and ${intro.asks.length - shown.length} more`);
246
+ if (intro.as.length) lines.push(`Signed in as ${intro.as.join(', then ')}`);
247
+ return lines.join('\n');
248
+ }