@simon-says-build/demo-player 0.2.0 → 0.3.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 +70 -6
- package/src/api.js +4 -0
- package/src/core.js +25 -0
package/index.d.ts
CHANGED
|
@@ -24,3 +24,8 @@ export function DemoHost(props: {
|
|
|
24
24
|
export function playDemo(demo: DemoBundle | null): boolean;
|
|
25
25
|
export function currentDemo(): { id: string; token: string; api: string } | null;
|
|
26
26
|
export function onDemo(fn: (demo: { id: string; token: string; api: string } | null) => void): () => void;
|
|
27
|
+
|
|
28
|
+
/** This player's version (0.3.0+), sent by the SSB overlay when it registers for pushes. */
|
|
29
|
+
export const PLAYER_VERSION: string;
|
|
30
|
+
/** The changes a demo's last card offers to approve or discuss: one per `job:<id>` feature. */
|
|
31
|
+
export function reviewChangesOf(demo: unknown): { jobId: string; title: string }[];
|
package/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { DemoHost } from './src/DemoHost.js';
|
|
2
|
-
export { demoConfig, findTarget, handlerFor, phoneSteps, scalePoint, playDemo, currentDemo, onDemo } from './src/core.js';
|
|
2
|
+
export { demoConfig, findTarget, handlerFor, phoneSteps, scalePoint, playDemo, currentDemo, onDemo, reviewChangesOf, PLAYER_VERSION } from './src/core.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.3.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
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
// for a demo sets). With one, it loads the demo's tour from SimonSays and walks
|
|
6
6
|
// the viewer through it: a ring on each step's element, a caption card with
|
|
7
7
|
// Back / Next, "Do it for me" on taps and typing, and a pill to tuck it away.
|
|
8
|
+
// A demo of CHANGES ends on a review card (0.3.0): ✓ Approve or 💬 Discuss
|
|
9
|
+
// each one, the extension's guided review on the phone (docs/GUIDED-REVIEW.md).
|
|
8
10
|
//
|
|
9
11
|
// No JSX, so it needs nothing from the app's Babel setup beyond React Native's.
|
|
10
12
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
11
|
-
import { Linking, Pressable, StyleSheet, Text, View, useWindowDimensions } from 'react-native';
|
|
12
|
-
import { cardBox, currentDemo, demoConfig, findTarget, handlerFor, onDemo, phoneSteps, playDemo, scalePoint } from './core.js';
|
|
13
|
+
import { Linking, Pressable, ScrollView, StyleSheet, Text, TextInput, View, useWindowDimensions } from 'react-native';
|
|
14
|
+
import { cardBox, currentDemo, demoConfig, findTarget, handlerFor, onDemo, phoneSteps, playDemo, reviewChangesOf, scalePoint } from './core.js';
|
|
13
15
|
import { demoApi, tourOf } from './api.js';
|
|
14
16
|
|
|
15
17
|
const h = React.createElement;
|
|
@@ -65,11 +67,13 @@ function Player({ config, router, insets, onClose, children }) {
|
|
|
65
67
|
const [card, setCard] = useState({ w: 320, h: 140 });
|
|
66
68
|
const [tucked, setTucked] = useState(false);
|
|
67
69
|
const [done, setDone] = useState(false);
|
|
70
|
+
const [changes, setChanges] = useState([]); // what the last card reviews (reviewChangesOf)
|
|
71
|
+
const [reviewing, setReviewing] = useState(false);
|
|
68
72
|
|
|
69
73
|
useEffect(() => {
|
|
70
74
|
let live = true;
|
|
71
75
|
api.load()
|
|
72
|
-
.then((d) => { if (live) setSteps(phoneSteps(tourOf(d, { here: !!config.here })).concat()); })
|
|
76
|
+
.then((d) => { if (live) { setSteps(phoneSteps(tourOf(d, { here: !!config.here })).concat()); setChanges(reviewChangesOf(d)); } })
|
|
73
77
|
.catch((e) => { if (live) setError(e.message); });
|
|
74
78
|
return () => { live = false; };
|
|
75
79
|
}, [api]);
|
|
@@ -106,6 +110,7 @@ function Player({ config, router, insets, onClose, children }) {
|
|
|
106
110
|
}, [step, done, screen.width, screen.height]);
|
|
107
111
|
|
|
108
112
|
const finish = useCallback(() => {
|
|
113
|
+
setReviewing(false);
|
|
109
114
|
setDone(true);
|
|
110
115
|
api.progress({ status: 'done' }).catch(() => {});
|
|
111
116
|
}, [api]);
|
|
@@ -116,9 +121,9 @@ function Player({ config, router, insets, onClose, children }) {
|
|
|
116
121
|
if (step.path.startsWith('/') && router?.push) router.push(step.path);
|
|
117
122
|
else Linking.openURL(step.path).catch(() => {});
|
|
118
123
|
}
|
|
119
|
-
if (index + 1 >= steps.length) finish();
|
|
124
|
+
if (index + 1 >= steps.length) { if (changes.length) setReviewing(true); else finish(); }
|
|
120
125
|
else setIndex(index + 1);
|
|
121
|
-
}, [steps, step, index, router, finish]);
|
|
126
|
+
}, [steps, step, index, router, finish, changes]);
|
|
122
127
|
|
|
123
128
|
const doIt = useCallback(() => {
|
|
124
129
|
if (!step || !found) return;
|
|
@@ -130,14 +135,16 @@ function Player({ config, router, insets, onClose, children }) {
|
|
|
130
135
|
const overlay = (() => {
|
|
131
136
|
if (error) return h(Card, { at: null, screen, insets, card, setCard, title: 'Demo', body: `The demo didn't load: ${error}` });
|
|
132
137
|
if (!steps) return null;
|
|
138
|
+
if (reviewing && !tucked) return h(Review, { api, changes, screen, insets, onBack: steps.length ? () => setReviewing(false) : null, onDone: finish, onHide: () => setTucked(true) });
|
|
133
139
|
if (tucked || done) {
|
|
134
140
|
// A demo started at run time closes when it is done (▶ Demo again replays it);
|
|
135
141
|
// one baked into a preview stays, to be replayed from its pill.
|
|
136
142
|
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
143
|
h(Text, { style: s.pillText }, 'Demo done · close'));
|
|
138
144
|
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' },
|
|
139
|
-
h(Text, { style: s.pillText }, done ? 'Demo done · replay' : `Demo ${index + 1}/${steps.length}`));
|
|
145
|
+
h(Text, { style: s.pillText }, done ? 'Demo done · replay' : reviewing ? 'Review the changes' : `Demo ${index + 1}/${steps.length}`));
|
|
140
146
|
}
|
|
147
|
+
if (!step && changes.length) return h(Review, { api, changes, screen, insets, onBack: null, onDone: finish, onHide: () => setTucked(true) });
|
|
141
148
|
if (!step) return h(Card, { at: null, screen, insets, card, setCard, title: 'Demo', body: 'This demo has no steps a phone can play.' });
|
|
142
149
|
const canDo = (step.kind === 'click' || step.kind === 'fill') && found && (step.kind === 'click' ? handlerFor(found, 'onPress') : handlerFor(found, 'onChangeText'));
|
|
143
150
|
return h(React.Fragment, null,
|
|
@@ -159,6 +166,55 @@ function Player({ config, router, insets, onClose, children }) {
|
|
|
159
166
|
h(View, { pointerEvents: 'box-none', style: StyleSheet.absoluteFill }, overlay));
|
|
160
167
|
}
|
|
161
168
|
|
|
169
|
+
// The review card: every change of the demo, each approved here or discussed
|
|
170
|
+
// with SSB. A word on a change is said in the conversation it came from, so
|
|
171
|
+
// SSB revises THAT change (functions/demo.mjs reviewRef). Nothing here holds
|
|
172
|
+
// the tour: Done closes it whatever is left undecided.
|
|
173
|
+
const ROW = { idle: null, approving: 'Approving…', approved: '✓ Approved', sending: 'Sending…', sent: 'Sent to SSB — it will revise this' };
|
|
174
|
+
function Review({ api, changes, screen, insets, onBack, onDone, onHide }) {
|
|
175
|
+
const [rows, setRows] = useState({}); // jobId → { state, error, text }
|
|
176
|
+
const set = (id, patch) => setRows((r) => ({ ...r, [id]: { ...(r[id] || {}), ...patch } }));
|
|
177
|
+
const approve = (id) => {
|
|
178
|
+
set(id, { state: 'approving', error: null });
|
|
179
|
+
api.approve(id).then(() => set(id, { state: 'approved' })).catch((e) => set(id, { state: 'idle', error: e.message }));
|
|
180
|
+
};
|
|
181
|
+
const send = (id) => {
|
|
182
|
+
const text = String(rows[id]?.text || '').trim();
|
|
183
|
+
if (!text) return;
|
|
184
|
+
set(id, { state: 'sending', error: null });
|
|
185
|
+
api.discuss(id, text).then(() => set(id, { state: 'sent' })).catch((e) => set(id, { state: 'discussing', error: e.message }));
|
|
186
|
+
};
|
|
187
|
+
const left = changes.filter((c) => !['approved', 'sent'].includes(rows[c.jobId]?.state)).length;
|
|
188
|
+
return h(View, { style: [s.card, s.review, { top: insets.top + 12, maxHeight: screen.height - insets.top - insets.bottom - 24, maxWidth: screen.width - 32, left: Math.max(16, (screen.width - Math.min(360, screen.width - 32)) / 2) }], testID: 'ssb-demo-review' },
|
|
189
|
+
h(Text, { style: s.title }, changes.length === 1 ? 'Review the change' : `Review ${changes.length} changes`),
|
|
190
|
+
h(Text, { style: s.body }, 'Approve what looks right. Anything that should be different, say so — SSB revises it.'),
|
|
191
|
+
h(ScrollView, { style: s.changes, keyboardShouldPersistTaps: 'handled' }, ...changes.map((c) => {
|
|
192
|
+
const r = rows[c.jobId] || {};
|
|
193
|
+
const state = r.state || 'idle';
|
|
194
|
+
return h(View, { key: c.jobId, style: s.change },
|
|
195
|
+
h(Text, { style: s.changeTitle }, c.title),
|
|
196
|
+
ROW[state] ? h(Text, { style: s.changeState }, ROW[state]) : null,
|
|
197
|
+
r.error ? h(Text, { style: s.error }, r.error) : null,
|
|
198
|
+
state === 'discussing' ? h(View, null,
|
|
199
|
+
h(TextInput, { style: s.input, value: r.text || '', onChangeText: (text) => set(c.jobId, { text }), placeholder: 'What should be different?', placeholderTextColor: '#8E8E93', multiline: true, autoFocus: true, accessibilityLabel: `What should be different about ${c.title}`, testID: `ssb-review-note-${c.jobId}` }),
|
|
200
|
+
h(View, { style: s.actions },
|
|
201
|
+
button('Cancel', () => set(c.jobId, { state: 'idle', error: null })),
|
|
202
|
+
button('Send', () => send(c.jobId), true))) : null,
|
|
203
|
+
state === 'idle' ? h(View, { style: s.actions },
|
|
204
|
+
button('💬 Discuss', () => set(c.jobId, { state: 'discussing' })),
|
|
205
|
+
button('✓ Approve', () => approve(c.jobId), true)) : null);
|
|
206
|
+
})),
|
|
207
|
+
h(View, { style: [s.actions, s.reviewFoot] },
|
|
208
|
+
button('Hide', onHide),
|
|
209
|
+
onBack ? button('Back', onBack) : null,
|
|
210
|
+
button(left ? `Done (${left} undecided)` : 'Done', onDone, !left)));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const button = (label, onPress, primary = false) => h(Pressable, {
|
|
214
|
+
key: label, onPress, accessibilityRole: 'button', accessibilityLabel: label,
|
|
215
|
+
style: ({ pressed }) => [s.button, primary && s.primary, pressed && s.pressed],
|
|
216
|
+
}, h(Text, { style: [s.buttonText, primary && s.primaryText] }, label));
|
|
217
|
+
|
|
162
218
|
function Card({ at, place, screen, insets, card, setCard, title, body, actions = [] }) {
|
|
163
219
|
const box = cardBox(place, at, screen, insets, card);
|
|
164
220
|
return h(View, {
|
|
@@ -189,6 +245,14 @@ const s = StyleSheet.create({
|
|
|
189
245
|
pressed: { opacity: 0.7 },
|
|
190
246
|
buttonText: { color: '#FFFFFF', fontSize: 14, fontWeight: '600' },
|
|
191
247
|
primaryText: { color: '#1C1C1E' },
|
|
248
|
+
review: { width: 360 },
|
|
249
|
+
changes: { flexGrow: 0 },
|
|
250
|
+
reviewFoot: { marginTop: 16 },
|
|
251
|
+
change: { marginTop: 14, paddingTop: 12, borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: '#3A3A3C' },
|
|
252
|
+
changeTitle: { color: '#FFFFFF', fontSize: 15, fontWeight: '600' },
|
|
253
|
+
changeState: { color: '#AEAEB2', fontSize: 13, marginTop: 4 },
|
|
254
|
+
error: { color: '#FF6961', fontSize: 13, marginTop: 4 },
|
|
255
|
+
input: { marginTop: 10, minHeight: 64, padding: 10, borderRadius: 10, backgroundColor: '#2C2C2E', color: '#FFFFFF', fontSize: 15 },
|
|
192
256
|
pill: { position: 'absolute', right: 16, paddingVertical: 8, paddingHorizontal: 14, borderRadius: 18, backgroundColor: '#1C1C1E' },
|
|
193
257
|
pillText: { color: '#FFFFFF', fontSize: 13, fontWeight: '600' },
|
|
194
258
|
});
|
package/src/api.js
CHANGED
|
@@ -13,6 +13,10 @@ export function demoApi({ api, id, token }, fetchImpl = globalThis.fetch) {
|
|
|
13
13
|
return {
|
|
14
14
|
load: () => call(''),
|
|
15
15
|
progress: (body) => call('/progress', { method: 'POST', body: JSON.stringify(body) }),
|
|
16
|
+
// Guided review (0.3.0): approve one change of this demo, or say something
|
|
17
|
+
// about it in the conversation it came from — SSB revises the change.
|
|
18
|
+
approve: (jobId) => call('/review', { method: 'POST', body: JSON.stringify({ jobId }) }),
|
|
19
|
+
discuss: (jobId, text) => call('/chat', { method: 'POST', body: JSON.stringify({ text, context: { about: jobId } }) }),
|
|
16
20
|
};
|
|
17
21
|
}
|
|
18
22
|
|
package/src/core.js
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
// raw text node.
|
|
8
8
|
import { bubbleBox } from './step-place.js';
|
|
9
9
|
|
|
10
|
+
// What this player is, said when the app registers for "Ready to see"
|
|
11
|
+
// (packages/ssb-overlay sends it; functions/in-app-notify.mjs clientOf). Kept
|
|
12
|
+
// equal to package.json by test/core.test.mjs.
|
|
13
|
+
export const PLAYER_VERSION = '0.3.0';
|
|
14
|
+
|
|
10
15
|
export const HOST = 5;
|
|
11
16
|
export const TEXT = 6;
|
|
12
17
|
|
|
@@ -133,3 +138,23 @@ export function playDemo(demo) {
|
|
|
133
138
|
}
|
|
134
139
|
export const currentDemo = () => runtime;
|
|
135
140
|
export function onDemo(fn) { listeners.add(fn); return () => listeners.delete(fn); }
|
|
141
|
+
|
|
142
|
+
// GUIDED REVIEW ON A PHONE (0.3.0, docs/GUIDED-REVIEW.md "On a phone"): the
|
|
143
|
+
// tour's last card lists the demo's CHANGES — each `job:<id>` feature, once,
|
|
144
|
+
// in the order the tour met them — to approve, or to discuss with SSB. A
|
|
145
|
+
// demo of no jobs (a feature demo) has nothing to review and just finishes.
|
|
146
|
+
export function reviewChangesOf(demo) {
|
|
147
|
+
const out = [];
|
|
148
|
+
const seen = new Set();
|
|
149
|
+
for (const a of demo?.actors || []) {
|
|
150
|
+
for (const sec of a.sections || []) {
|
|
151
|
+
for (const f of sec.features || []) {
|
|
152
|
+
const m = /^job:(.+)$/.exec(String(f?.featureId || ''));
|
|
153
|
+
if (!m || seen.has(m[1])) continue;
|
|
154
|
+
seen.add(m[1]);
|
|
155
|
+
out.push({ jobId: m[1], title: String(f.title || 'This change') });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|