@navipartner/demo-video 0.4.0 → 0.5.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.
@@ -1,348 +1,364 @@
1
- ---
2
- name: np-demo-video
3
- description: Produce or change a marketing demo video of a NaviPartner React app (retail self-checkout, food-order kiosk, or any app with a demo/ adapter) in the house look, which is calm camera pushes, serif statement cards and customer branding, recorded by the engine @navipartner/demo-video. Use when asked for a product demo video, a showcase of a flow, a marketing or SoMe clip, a customer-branded video (e.g. "two waters, in a customer's branding"), or to re-record a demo after UI changes.
4
- ---
5
-
6
- # NaviPartner demo video
7
-
8
- You direct; the engine (`@navipartner/demo-video`) films. You write a story
9
- (JSON). Never write Playwright code for a video and never edit the app to
10
- make a video pass.
11
-
12
- **Read [look.md](look.md) first.** It defines how every video must look and
13
- feel: chapters, statement cards, camera, cursor, branding. This file is the
14
- workflow.
15
-
16
- ## 1. Understand the request
17
-
18
- From the request, take:
19
- - **Product:** which app.
20
- - **Chapters:** the 1–4 ideas to show.
21
- - **Brand:** default NaviPartner.
22
- - **Data:** the products, names and amounts the request mentions ("two
23
- waters").
24
-
25
- Card text defaults to English. Ask only for what you cannot infer.
26
-
27
- ### The primary story
28
-
29
- Every app has one **primary story**: the journey the app exists for, from
30
- start to the moment of value. It is the app's first story, and the one filmed
31
- when a request names no feature ("make a video of the QR app", "a video in
32
- the customer's branding").
33
-
34
- | App | Primary story | Ends on |
35
- |---|---|---|
36
- | Self-service food order | order and pay | "Thank you", order number |
37
- | QR food order | order from the table | order received |
38
- | Retail self-checkout | scan and pay | paid, receipt |
39
- | NP Passes | create a pass | the pass is created |
40
-
41
- To find it in a new app:
42
- - **The e2e suite's happy path.** There is almost always a spec that walks the
43
- whole flow (`complete-order-flow.spec.ts`, `06-order-complete.spec.ts`).
44
- Follow it, and end where it asserts success.
45
- - **The app's purpose.** Its README, routes and package description say what
46
- it is for. If they disagree with the happy-path spec, ask.
47
-
48
- Name the story for that job (`checkout`, `create-pass`) and mark it
49
- `"primary": true`. One story per app may be primary; `demo-video list` refuses
50
- two. In the PR, say which spec the story follows, so the developer can confirm
51
- it is the right journey.
52
-
53
- Run `pnpm --dir <app> exec demo-video list` first. If a primary story exists and the
54
- request names no feature, render it (`pnpm --dir <app> exec demo-video render` without `--story`,
55
- with `--brand` if one is asked for) instead of writing a new one. For a
56
- repo with several apps and no app named, ask which app, or offer each app's
57
- primary story. A PR never films it by default; only stories a PR adds or
58
- changes are filmed there.
59
-
60
- ## 2. Plan the chapters on paper first
61
-
62
- Before any JSON, write the plan. For each chapter give, in this order:
63
- 1. **What the viewer sees happen**, as one plain sentence with the chapter's
64
- verb: "the shopper scans an item and it lands in the basket".
65
- 2. **The actions** (2–5), and for each one **what it changes and where on
66
- screen** that change appears (a new row at the top, the total at the
67
- bottom, a new screen).
68
- 3. **The shot:** one push-in on the area that holds a control *and* what it
69
- changes, or wide when they're far apart. The camera never chases the
70
- cursor.
71
- 4. **The statement card**, written last, from sentence 1 (see "Writing the
72
- cards").
73
- 5. **The payoff fact**, if there is one: a value the app shows, checked with
74
- `expectText`.
75
-
76
- Check it against look.md:
77
- - Is the story clear on mute?
78
- - Does every card describe exactly what the chapter after it shows?
79
- - Does the hook land by second 6?
80
-
81
- Example, retail self-checkout for a customer (here the made-up brand Acme):
82
-
83
- ```
84
- open Introducing · Acme logo · sub "Self-checkout"
85
- ch1 sees: a guest scans two waters and they land in the basket
86
- shots: wide for the scans (the rows appear at the top);
87
- push in 1.5 on the basket rows to read them
88
- card: "Scan *and go*."
89
- ch2 sees: the guest pays by card and the payment goes through
90
- shots: push in 1.6 on "Go to payment" (button + total);
91
- pull out when the payment screen opens
92
- card: "Pay *by card*."
93
- payoff "Paid *in seconds*." · sub "64,50 kr. by card." (expectText on the total)
94
- end Acme logo · "Self-checkout by NaviPartner"
95
- ```
96
-
97
- ## 3. Learn the app
98
-
99
- 1. **Adapter:** `<app>/demo/demo.config.ts`. It says how the app runs
100
- offline (`setup` → the e2e mocks), its `ready` testid and any
101
- `actions` (e.g. retail's `scan`). No adapter yet: see "A new app" below.
102
- 2. **Testids:** read the components for every screen in the plan and collect
103
- their `data-testid`s.
104
- 3. **Data:** a story is inspired only by what the project's Playwright
105
- e2e tests already have: their mocks, logins and data. Read the e2e specs
106
- (they already walk the flows) and the mocks `setup` calls. Only data they
107
- return can appear (items, barcodes, prices, names).
108
- - Never add fixtures or change the mocks or the app for a video.
109
- - Skip the test edge cases the mocks contain on purpose, such as a product
110
- with an absurdly long name or an error item.
111
- - If the request needs data the e2e tests don't have, say so instead of
112
- inventing it.
113
- - One exception: a customer brand may rename the mock's products in its
114
- `config.demoData` (see "A new brand"). It gives them names only, never
115
- new barcodes, prices or flows.
116
-
117
- ### A new app
118
-
119
- 1. From the repo root: `npx --ignore-scripts @navipartner/demo-video init --app <app>`, then
120
- `pnpm install`. (A repo set up for the first time runs
121
- `npx --ignore-scripts @navipartner/demo-video init` without `--app` first.)
122
- It adds the
123
- dependency and writes a `demo.config.ts` skeleton with the dev server
124
- from `playwright.config`.
125
- 2. Finish the skeleton from the app's **current** code and e2e tests, not
126
- from notes or an older adapter:
127
- - `appPath` and `ready`: what the e2e helpers open and wait for.
128
- - `setup`: the e2e mocks exactly as the tests call them.
129
- - The brand: find the response that carries the app's branding and the
130
- code that parses it. Lay the brand over it with `overlayResponse`,
131
- using only keys that code accepts (colour formats, asset fields).
132
- - `actions` for what a test does through code (a scanner bridge, a test
133
- dispatch), and `idleTimeoutSeconds` if the app resets when idle.
134
- 3. Write the primary story (above), then `pnpm --dir <app> exec demo-video check`.
135
-
136
- ## 4. Write the story
137
-
138
- A developer writes stories locally (VS Code, with this skill) and a PR
139
- confirms them. A `/demo-video` PR comment, and later the Linear agent, only
140
- pick an existing story and a brand: they never write one. A story is
141
- `<app>/demo/stories/<name>.json`, named for what it shows (`checkout`,
142
- `settings`), and holds no brand: the same story renders for any brand with
143
- `--brand <name>`. A customer's own words go in a variant (below).
144
-
145
- { "app": "<adapter app>",
146
- "description": "One sentence: what the story shows. Follows e2e/<spec>.",
147
- "primary": true,
148
- "title": { "eyebrow": "Introducing", "logo": true, "sub": "…" },
149
- "closing": { "logo": true, "sub": "…" },
150
- "steps": [ … ] }
151
-
152
- Steps: `click` `type` `press` `zoom` `frame` `zoomOut` `park` `waitFor`
153
- `expectText` `hold` `card`, plus the adapter's actions (every field in
154
- [reference.md](reference.md)).
155
-
156
- How the look maps onto steps:
157
-
158
- - **Cards:** `{ "do": "card", "heading": "Pay *by card*." }` opens a
159
- chapter; `"style": "brand"` is the full-bleed payoff. A card stays up at
160
- least as long as it takes to read, so `seconds` is only needed to hold one
161
- longer.
162
- - **Push in:** `{ "do": "zoom", "target": "…", "scale": 1.6 }` on the area
163
- where the action *and its result* are, then hold steady through the
164
- clicks there. Use 1.4–1.8 (the engine accepts 1.3–2). `"zoom"` on a click
165
- pushes in on that control first. At most two push-ins a chapter.
166
- - **Frame the action and its result together:**
167
- `{ "do": "frame", "targets": ["pay-button", "order-total"] }` computes one
168
- shot that holds them all (2–4 targets, each a testid or
169
- `{ "target", "nth" }`), pushed in as far as they fit (at most 1.8, or
170
- `maxScale`). Prefer it to guessing one zoom target. Targets too far apart
171
- for a push-in fail `check`: frame things that sit together.
172
- - **A result elsewhere:** when a click changes something outside the shot (a
173
- row added at the top, a new screen), pull out right after it
174
- (`zoomOut`), or frame both from the start. `check` fails a click whose
175
- change the viewer never sees.
176
- - **Read:** `hold` 1.5–3 s on anything the viewer must read (a total, a
177
- confirmation).
178
- - **Leave the beats to the engine.** It pauses on each target before the
179
- press, holds the press, waits for the app to react, settles the camera
180
- before acting, lingers on a chapter's result before its card, and opens
181
- every chapter wide with the cursor at rest. Don't add holds for those.
182
- - **Works with every brand:** any story can be filmed with any brand (the
183
- label uses the app's default). Never target text a brand's `demoData`
184
- sets (renamed products, an order number): pick products by testid and
185
- `nth`, the mock's order. Run `check` without `--brand` as well as with the
186
- brand you're writing for.
187
- - **Safety:** after each navigation, `waitFor` the next screen's testid. Put
188
- `expectText` on prices and confirmations so the video fails if the copy is
189
- wrong, and so a card may quote them.
190
-
191
- ### Writing the cards
192
-
193
- The words are what the viewer remembers; write them with care.
194
-
195
- - **A statement names what the chapter shows,** in the chapter's own verb,
196
- before it happens: a scan chapter is "Scan *and go*.", not "Shopping made
197
- easy." If the viewer couldn't point at it in the next ten seconds, rewrite
198
- it.
199
- - **The italic word is the action or its result** ("*scan*", "*by card*",
200
- "*paid*"), never a filler word.
201
- - **A payoff states the result just shown,** with the app's real values from
202
- an `expectText`: "64,50 kr. by card." No durations, counts or claims the
203
- video doesn't show; `check` rejects a number no `expectText` proves.
204
- - **Title sub:** what the product is ("Self-checkout"). **Closing sub:** one
205
- short line, the product's promise.
206
- - **Never on a card:** testids, barcodes, internal names, "demo", "test",
207
- exclamation marks, "seamless", "effortless", "revolutionary".
208
- - Statements ≤ 7 words, subs ≤ 16 (`check` stops at 8 and 16), sentence
209
- case, ending with a full stop.
210
-
211
- ### A new brand
212
-
213
- A customer brand is `<app>/demo/brands/<name>.json` with its assets in
214
- `<app>/demo/brands/<name>/`. Fields: [reference.md](reference.md).
215
-
216
- A customer's name, logo and look are their trademarks. A video in them is
217
- made with their permission, from what they gave us, and stays internal until
218
- they approve it.
219
-
220
- - **Only with permission.** Make a customer brand only when the request says
221
- the customer has agreed to a video in their branding (an account manager or
222
- the customer asked for it). If that's unclear, ask before you start.
223
- - **Only their own material, never invented or recreated:** the logo file and
224
- brand guide the customer supplied, or that a NaviPartner colleague received
225
- from them. Don't download, trace, redraw or recolour a logo from their
226
- website, and don't guess a colour or font. If a value is missing, ask the
227
- customer contact for it; colours from their public site are a stand-in to
228
- confirm with them, never final.
229
- - **Say where it came from.** The PR that adds the brand names the source of
230
- the logo, colours and fonts, and who gave permission. Without that, the
231
- brand doesn't merge.
232
- - **Logo:** an SVG or a PNG with a transparent background, readable on a
233
- light background (`logoColor`, a top-level key, recolours a white SVG; use
234
- it only when their brand guide allows that colour version).
235
- - **Fonts:** only a font whose licence covers use in a video. Google Fonts are
236
- open-licensed and fine. A customer's own or commercial font is used only if
237
- they confirm their licence covers it; otherwise take the closest Google
238
- Font and say so in the report. Never copy font files from their website.
239
- - **No claims for them:** cards state what the video shows. No customer
240
- quotes, statistics or "trusted by" lines, and a closing card says what the
241
- product is, not that the customer endorses it.
242
- - **Publishing:** a customer-branded video goes to the customer for approval
243
- before it is published anywhere. Say so in the report.
244
- - **`config`:** only keys the app's branding code accepts. Override every
245
- value the e2e mock sets that would show through, such as a mock
246
- `backgroundColor`.
247
- - **`config.demoData`** (when the adapter supports it): the customer's names
248
- for the mock's products, keyed by the mock's barcodes. Mock data such as
249
- "Toothpaste" or "TEST CARD" reads as a test run in a customer video. Use
250
- plain product names ("Lemonade"), or ones the customer gave you, never a
251
- third party's brand ("Coca-Cola"). Keep the mock's prices, so pick
252
- products that are believable at those prices, and put the totals on the
253
- cards from the render, not from your own sums.
254
- - **App limits:** an app may only offer presets (retail has two font pairs).
255
- Pick the closest and say in the report what the kiosk can't show.
256
- - **`video` fonts** load from Google Fonts: the heading in 400, 700 and
257
- italic 400, the body in 400 and 600. If a font lacks one, give `fontsUrl`
258
- yourself.
259
- - Render with `--brand <name>` and check the stills before calling it done.
260
-
261
- ### A variant: the story in a customer's words
262
-
263
- When a customer video needs its own words (their product, their place), write
264
- a variant beside the story instead of a copy: `checkout.acme.json`.
265
-
266
- { "extends": "checkout",
267
- "brand": "acme",
268
- "description": "The checkout story, for Acme.",
269
- "title": { "sub": "Self-service at Acme" },
270
- "closing": { "sub": "…" },
271
- "cards": [ { "heading": "A burger *and a lemonade*." }, { … }, { … } ] }
272
-
273
- - The steps are always the base story's: a variant changes words and brand,
274
- never the flow. Fix the flow in the base, once, for every variant.
275
- - `cards` gives every card step of the base, in order; each entry, like
276
- `title` and `closing`, changes only the fields it names. When the base
277
- gains or loses a card, the variant stops loading until it is updated.
278
- - It films with its own brand only. `/demo-video checkout in acme branding`
279
- and the form (story `checkout`, brand `acme`) pick the variant.
280
- - The same rules as any card: numbers proven by the base's `expectText`s,
281
- and the customer's permission for their name (see "A new brand").
282
- - `check --story checkout.acme` before the PR, and re-check the variants
283
- when you change their base.
284
-
285
- ### Not supported yet
286
-
287
- There's no music bed. Videos are silent, so they must work on mute.
288
-
289
- ## 5. Check, then render
290
-
291
- pnpm --dir <app> exec demo-video check [--story <name>] --json
292
-
293
- Without `--story`, commands use the primary story. `check` performs every
294
- step for real and fails on anything that would film badly:
295
-
296
- | It says | Fix |
297
- |---|---|
298
- | `"x" is outside the shot …` | The zoom frames the wrong area: zoom on what this shot clicks, or `zoomOut` first |
299
- | `clicking "x" changes y, which is outside the shot …` | Frame what the click changes, or `zoomOut` right after the click |
300
- | `N camera push-ins in one chapter` | Push in once on the chapter's area and hold steady |
301
- | `… only fit in the wide shot` | `frame` things that sit together, or stay wide |
302
- | `… gives N cards; … has M` | Give the variant one entry per card of its story, in order |
303
- | `a zoom with nothing in its shot` | Remove it, or act or read something in it |
304
- | `zoom 2.4; use 1.3–2` | Zoom less: context reads better and stays sharp |
305
- | `"…" looks like a testid-like name / barcode / code` | Cards are for people: say it in words |
306
- | `"38" isn't shown by an expectText` | Quote only values an `expectText` checks |
307
- | `the card announces a chapter in which nothing is done` | Follow it with the steps it describes |
308
-
309
- Fix what the report names. Its `visible` list shows what is on screen at the
310
- failing step. Repeat until it passes, at most 3 rounds; a story that still
311
- fails is reported as "couldn't show this", with the last report. Then:
312
-
313
- pnpm --dir <app> exec demo-video render [--story <name>] [--brand <name>]
314
-
315
- Rendering films every frame (60 fps, frame by frame), so it takes about
316
- five times the video's length. While tuning, `render --draft` films at
317
- 30 fps in about half the time (`…-draft-demo.mp4`); render the final without
318
- it. Playwright starts the app's dev server from
319
- the adapter's `webServer`; if it can't on this machine, start it yourself
320
- with the same command and env, and it is reused.
321
-
322
- ## 6. Review like a viewer
323
-
324
- Open `<app>/demo/out/review/sheet.jpg` first: one frame a second, the whole
325
- video at a glance. Then the full-size stills beside it. Judge against
326
- look.md:
327
- - **Story:** each statement card is followed by exactly what it says, and
328
- the payoff by the result it states.
329
- - **Camera:** every push-in lands where something then happens; no shot is
330
- mostly empty space; each click's result is on screen.
331
- - **Framing:** pushed-in shots fill the frame and read on a phone; nothing
332
- important is cut off; the cursor rests out of the way.
333
- - **Pacing:** each chapter is one idea; nothing rushed, nothing lingering.
334
- - **Truth:** every word is true of what's on screen.
335
-
336
- Adjust the JSON and re-render. Two rounds of tuning is normal; if it takes
337
- more, the plan from step 2 is the problem.
338
-
339
- ## Report
340
-
341
- Give:
342
- - the MP4 path and duration
343
- - the chapter plan
344
- - the contact sheet and stills
345
- - which brand it was rendered with, and for a customer brand, that the
346
- video needs the customer's approval before it is published
347
- - anything the video could not show truthfully (it shows demo fixture data,
348
- not a real tenant's)
1
+ ---
2
+ name: np-demo-video
3
+ description: Produce or change a marketing demo video of a NaviPartner React app (retail self-checkout, food-order kiosk, or any app with a demo/ adapter) in the house look, which is calm camera pushes, serif statement cards and customer branding, recorded by the engine @navipartner/demo-video. Use when asked for a product demo video, a showcase of a flow, a marketing or SoMe clip, a customer-branded video (e.g. "two waters, in a customer's branding"), a pitch draft for a prospective customer (e.g. "pitch draft of checkout for Hotel Royal"; follow pitch.md), or to re-record a demo after UI changes.
4
+ ---
5
+
6
+ # NaviPartner demo video
7
+
8
+ You direct; the engine (`@navipartner/demo-video`) films. You write a story
9
+ (JSON). Never write Playwright code for a video and never edit the app to
10
+ make a video pass.
11
+
12
+ **Read [look.md](look.md) first.** It defines how every video must look and
13
+ feel: chapters, statement cards, camera, cursor, branding. This file is the
14
+ workflow.
15
+
16
+ ## 1. Understand the request
17
+
18
+ From the request, take:
19
+ - **Product:** which app.
20
+ - **Chapters:** the 1–4 ideas to show.
21
+ - **Brand:** default NaviPartner.
22
+ - **Data:** the products, names and amounts the request mentions ("two
23
+ waters").
24
+
25
+ Card text defaults to English. Ask only for what you cannot infer.
26
+
27
+ ### The primary story
28
+
29
+ Every app has one **primary story**: the journey the app exists for, from
30
+ start to the moment of value. It is the app's first story, and the one filmed
31
+ when a request names no feature ("make a video of the QR app", "a video in
32
+ the customer's branding").
33
+
34
+ | App | Primary story | Ends on |
35
+ |---|---|---|
36
+ | Self-service food order | order and pay | "Thank you", order number |
37
+ | QR food order | order from the table | order received |
38
+ | Retail self-checkout | scan and pay | paid, receipt |
39
+ | NP Passes | create a pass | the pass is created |
40
+
41
+ To find it in a new app:
42
+ - **The e2e suite's happy path.** There is almost always a spec that walks the
43
+ whole flow (`complete-order-flow.spec.ts`, `06-order-complete.spec.ts`).
44
+ Follow it, and end where it asserts success.
45
+ - **The app's purpose.** Its README, routes and package description say what
46
+ it is for. If they disagree with the happy-path spec, ask.
47
+
48
+ Name the story for that job (`checkout`, `create-pass`) and mark it
49
+ `"primary": true`. One story per app may be primary; `demo-video list` refuses
50
+ two. In the PR, say which spec the story follows, so the developer can confirm
51
+ it is the right journey.
52
+
53
+ Run `pnpm --dir <app> exec demo-video list` first. If a primary story exists and the
54
+ request names no feature, render it (`pnpm --dir <app> exec demo-video render` without `--story`,
55
+ with `--brand` if one is asked for) instead of writing a new one. For a
56
+ repo with several apps and no app named, ask which app, or offer each app's
57
+ primary story. A PR never films it by default; only stories a PR adds or
58
+ changes are filmed there.
59
+
60
+ ## 2. Plan the chapters on paper first
61
+
62
+ Before any JSON, write the plan. For each chapter give, in this order:
63
+ 1. **What the viewer sees happen**, as one plain sentence with the chapter's
64
+ verb: "the shopper scans an item and it lands in the basket".
65
+ 2. **The actions** (2–5), and for each one **what it changes and where on
66
+ screen** that change appears (a new row at the top, the total at the
67
+ bottom, a new screen).
68
+ 3. **The shot:** one push-in on the area that holds a control *and* what it
69
+ changes, or wide when they're far apart. The camera never chases the
70
+ cursor.
71
+ 4. **The statement card**, written last, from sentence 1 (see "Writing the
72
+ cards").
73
+ 5. **The payoff fact**, if there is one: a value the app shows, checked with
74
+ `expectText`.
75
+
76
+ Check it against look.md:
77
+ - Is the story clear on mute?
78
+ - Does every card describe exactly what the chapter after it shows?
79
+ - Does the hook land by second 6?
80
+
81
+ Example, retail self-checkout for a customer (here the made-up brand Acme):
82
+
83
+ ```
84
+ open Introducing · Acme logo · sub "Self-checkout"
85
+ ch1 sees: a guest scans two waters and they land in the basket
86
+ shots: wide for the scans (the rows appear at the top);
87
+ push in 1.5 on the basket rows to read them
88
+ card: "Scan *and go*."
89
+ ch2 sees: the guest pays by card and the payment goes through
90
+ shots: push in 1.6 on "Go to payment" (button + total);
91
+ pull out when the payment screen opens
92
+ card: "Pay *by card*."
93
+ payoff "Paid *in seconds*." · sub "64,50 kr. by card." (expectText on the total)
94
+ end Acme logo · "Self-checkout by NaviPartner"
95
+ ```
96
+
97
+ ## 3. Learn the app
98
+
99
+ 1. **Adapter:** `<app>/demo/demo.config.ts`. It says how the app runs
100
+ offline (`setup` → the e2e mocks), its `ready` testid and any
101
+ `actions` (e.g. retail's `scan`). No adapter yet: see "A new app" below.
102
+ 2. **Testids:** read the components for every screen in the plan and collect
103
+ their `data-testid`s.
104
+ 3. **Data:** a story is inspired only by what the project's Playwright
105
+ e2e tests already have: their mocks, logins and data. Read the e2e specs
106
+ (they already walk the flows) and the mocks `setup` calls. Only data they
107
+ return can appear (items, barcodes, prices, names).
108
+ - Never add fixtures or change the mocks or the app for a video.
109
+ - Skip the test edge cases the mocks contain on purpose, such as a product
110
+ with an absurdly long name or an error item.
111
+ - If the request needs data the e2e tests don't have, say so instead of
112
+ inventing it.
113
+ - One exception: a customer brand may rename the mock's products in its
114
+ `config.demoData` (see "A new brand"). It gives them names only, never
115
+ new barcodes, prices or flows.
116
+
117
+ ### A new app
118
+
119
+ 1. From the repo root: `npx --ignore-scripts @navipartner/demo-video init`,
120
+ then `pnpm install`. It finds the apps (folders with a
121
+ `playwright.config`). With one app (at the repo root or in a folder) it
122
+ sets that app up too; with several it lists them, and you run
123
+ `init --app <folder>` for this one. Setting an app up adds the dependency
124
+ and writes a `demo.config.ts` skeleton with the dev server from
125
+ `playwright.config`.
126
+ 2. Finish the skeleton from the app's **current** code and e2e tests, not
127
+ from notes or an older adapter:
128
+ - `appPath` and `ready`: what the e2e helpers open and wait for.
129
+ - `setup`: the e2e mocks exactly as the tests call them.
130
+ - The brand: find the response that carries the app's branding and the
131
+ code that parses it. Lay the brand over it with `overlayResponse`,
132
+ using only keys that code accepts (colour formats, asset fields).
133
+ - `brandNeeds`: every `config` key and asset slot `setup` uses, each
134
+ with what it is and where it shows ("full-screen photo behind the
135
+ menu, landscape"). A brand without one fails `check`, and a pitch
136
+ draft's research fills exactly these, so be specific. Mark a need the
137
+ app can do without `optional: true`. `demo-video brand-needs` prints
138
+ them.
139
+ - `actions` for what a test does through code (a scanner bridge, a test
140
+ dispatch), and `idleTimeoutSeconds` if the app resets when idle.
141
+ 3. Write the primary story (above), then `pnpm --dir <app> exec demo-video check`.
142
+
143
+ ## 4. Write the story
144
+
145
+ A developer writes stories locally (VS Code, with this skill) and a PR
146
+ confirms them. A `/demo-video` comment (on a PR, an issue, or from Linear's
147
+ synced thread) and the Run workflow form only
148
+ pick an existing story and a brand: they never write one. A story is
149
+ `<app>/demo/stories/<name>.json`, named for what it shows (`checkout`,
150
+ `settings`), and holds no brand: the same story renders for any brand with
151
+ `--brand <name>`. A customer's own words go in a variant (below).
152
+
153
+ { "app": "<adapter app>",
154
+ "description": "One sentence: what the story shows. Follows e2e/<spec>.",
155
+ "primary": true,
156
+ "title": { "eyebrow": "Introducing", "logo": true, "sub": "…" },
157
+ "closing": { "logo": true, "sub": "…" },
158
+ "steps": [ … ] }
159
+
160
+ Steps: `click` `type` `press` `zoom` `frame` `zoomOut` `park` `waitFor`
161
+ `expectText` `hold` `card`, plus the adapter's actions (every field in
162
+ [reference.md](reference.md)).
163
+
164
+ How the look maps onto steps:
165
+
166
+ - **Cards:** `{ "do": "card", "heading": "Pay *by card*." }` opens a
167
+ chapter; `"style": "brand"` is the full-bleed payoff. A card stays up at
168
+ least as long as it takes to read, so `seconds` is only needed to hold one
169
+ longer.
170
+ - **Push in:** `{ "do": "zoom", "target": "…", "scale": 1.6 }` on the area
171
+ where the action *and its result* are, then hold steady through the
172
+ clicks there. Use 1.4–1.8 (the engine accepts 1.3–2). `"zoom"` on a click
173
+ pushes in on that control first. At most two push-ins a chapter.
174
+ - **Frame the action and its result together:**
175
+ `{ "do": "frame", "targets": ["pay-button", "order-total"] }` computes one
176
+ shot that holds them all (2–4 targets, each a testid or
177
+ `{ "target", "nth" }`), pushed in as far as they fit (at most 1.8, or
178
+ `maxScale`). Prefer it to guessing one zoom target. Targets too far apart
179
+ for a push-in fail `check`: frame things that sit together.
180
+ - **A result elsewhere:** when a click changes something outside the shot (a
181
+ row added at the top, a new screen), pull out right after it
182
+ (`zoomOut`), or frame both from the start. `check` fails a click whose
183
+ change the viewer never sees.
184
+ - **Read:** `hold` 1.5–3 s on anything the viewer must read (a total, a
185
+ confirmation).
186
+ - **Leave the beats to the engine.** It pauses on each target before the
187
+ press, holds the press, waits for the app to react, settles the camera
188
+ before acting, lingers on a chapter's result before its card, and opens
189
+ every chapter wide with the cursor at rest. Don't add holds for those.
190
+ - **Works with every brand:** any story can be filmed with any brand (the
191
+ label uses the app's default). Never target text a brand's `demoData`
192
+ sets (renamed products, an order number): pick products by testid and
193
+ `nth`, the mock's order. Run `check` without `--brand` as well as with the
194
+ brand you're writing for.
195
+ - **Safety:** after each navigation, `waitFor` the next screen's testid. Put
196
+ `expectText` on prices and confirmations so the video fails if the copy is
197
+ wrong, and so a card may quote them.
198
+
199
+ ### Writing the cards
200
+
201
+ The words are what the viewer remembers; write them with care.
202
+
203
+ - **A statement names what the chapter shows,** in the chapter's own verb,
204
+ before it happens: a scan chapter is "Scan *and go*.", not "Shopping made
205
+ easy." If the viewer couldn't point at it in the next ten seconds, rewrite
206
+ it.
207
+ - **The italic word is the action or its result** ("*scan*", "*by card*",
208
+ "*paid*"), never a filler word.
209
+ - **A payoff states the result just shown,** with the app's real values from
210
+ an `expectText`: "64,50 kr. by card." No durations, counts or claims the
211
+ video doesn't show; `check` rejects a number no `expectText` proves.
212
+ - **Title sub:** what the product is ("Self-checkout"). **Closing sub:** one
213
+ short line, the product's promise.
214
+ - **Never on a card:** testids, barcodes, internal names, "demo", "test",
215
+ exclamation marks, "seamless", "effortless", "revolutionary".
216
+ - Statements ≤ 7 words, subs ≤ 16 (`check` stops at 8 and 16), sentence
217
+ case, ending with a full stop.
218
+
219
+ ### A new brand
220
+
221
+ A customer brand is `<app>/demo/brands/<name>.json` with its assets in
222
+ `<app>/demo/brands/<name>/`. Fields: [reference.md](reference.md).
223
+
224
+ A customer's name, logo and look are their trademarks. A video in them is
225
+ made with their permission, from what they gave us, and stays internal until
226
+ they approve it.
227
+
228
+ - **Only with permission.** Make a customer brand only when the request says
229
+ the customer has agreed to a video in their branding (an account manager or
230
+ the customer asked for it). If that's unclear, ask before you start.
231
+ - **Only their own material, never invented or recreated:** the logo file and
232
+ brand guide the customer supplied, or that a NaviPartner colleague received
233
+ from them. Don't download, trace, redraw or recolour a logo from their
234
+ website, and don't guess a colour or font. If a value is missing, ask the
235
+ customer contact for it; colours from their public site are a stand-in to
236
+ confirm with them, never final.
237
+ - **Say where it came from.** The PR that adds the brand names the source of
238
+ the logo, colours and fonts, and who gave permission. Without that, the
239
+ brand doesn't merge.
240
+ - **Logo:** an SVG or a PNG with a transparent background, readable on a
241
+ light background (`logoColor`, a top-level key, recolours a white SVG; use
242
+ it only when their brand guide allows that colour version).
243
+ - **Fonts:** only a font whose licence covers use in a video. Google Fonts are
244
+ open-licensed and fine. A customer's own or commercial font is used only if
245
+ they confirm their licence covers it; otherwise take the closest Google
246
+ Font and say so in the report. Never copy font files from their website.
247
+ - **No claims for them:** cards state what the video shows. No customer
248
+ quotes, statistics or "trusted by" lines, and a closing card says what the
249
+ product is, not that the customer endorses it.
250
+ - **Publishing:** a customer-branded video goes to the customer for approval
251
+ before it is published anywhere. Say so in the report.
252
+ - **`config`:** only keys the app's branding code accepts. Override every
253
+ value the e2e mock sets that would show through, such as a mock
254
+ `backgroundColor`.
255
+ - **`config.demoData`** (when the adapter supports it): the customer's names
256
+ for the mock's products, keyed by the mock's barcodes. Mock data such as
257
+ "Toothpaste" or "TEST CARD" reads as a test run in a customer video. Use
258
+ plain product names ("Lemonade"), or ones the customer gave you, never a
259
+ third party's brand ("Coca-Cola"). Keep the mock's prices, so pick
260
+ products that are believable at those prices, and put the totals on the
261
+ cards from the render, not from your own sums.
262
+ - **App limits:** an app may only offer presets (retail has two font pairs).
263
+ Pick the closest and say in the report what the kiosk can't show.
264
+ - **`video` fonts** load from Google Fonts: the heading in 400, 700 and
265
+ italic 400, the body in 400 and 600. If a font lacks one, give `fontsUrl`
266
+ yourself.
267
+ - Render with `--brand <name>` and check the stills before calling it done.
268
+
269
+ ### Pitch drafts
270
+
271
+ Asked for a **pitch draft** (an existing story in a prospective customer's
272
+ branding, usually in a Linear session): follow [pitch.md](pitch.md) and
273
+ nothing else here. A pitch writes nothing to the repo, and its researched
274
+ brand is not a brand: never commit it or copy its values into one. When the
275
+ customer agrees to more, make a real brand with the rules above.
276
+
277
+ ### A variant: the story in a customer's words
278
+
279
+ When a customer video needs its own words (their product, their place), write
280
+ a variant beside the story instead of a copy: `checkout.acme.json`.
281
+
282
+ { "extends": "checkout",
283
+ "brand": "acme",
284
+ "description": "The checkout story, for Acme.",
285
+ "title": { "sub": "Self-service at Acme" },
286
+ "closing": { "sub": "…" },
287
+ "cards": [ { "heading": "A burger *and a lemonade*." }, { … }, { … } ] }
288
+
289
+ - The steps are always the base story's: a variant changes words and brand,
290
+ never the flow. Fix the flow in the base, once, for every variant.
291
+ - `cards` gives every card step of the base, in order; each entry, like
292
+ `title` and `closing`, changes only the fields it names. When the base
293
+ gains or loses a card, the variant stops loading until it is updated.
294
+ - It films with its own brand only. `/demo-video checkout in acme branding`
295
+ and the form (story `checkout`, brand `acme`) pick the variant.
296
+ - The same rules as any card: numbers proven by the base's `expectText`s,
297
+ and the customer's permission for their name (see "A new brand").
298
+ - `check --story checkout.acme` before the PR, and re-check the variants
299
+ when you change their base.
300
+
301
+ ### Not supported yet
302
+
303
+ There's no music bed. Videos are silent, so they must work on mute.
304
+
305
+ ## 5. Check, then render
306
+
307
+ pnpm --dir <app> exec demo-video check [--story <name>] --json
308
+
309
+ Without `--story`, commands use the primary story. `check` performs every
310
+ step for real and fails on anything that would film badly:
311
+
312
+ | It says | Fix |
313
+ |---|---|
314
+ | `"x" is outside the shot …` | The zoom frames the wrong area: zoom on what this shot clicks, or `zoomOut` first |
315
+ | `clicking "x" changes y, which is outside the shot …` | Frame what the click changes, or `zoomOut` right after the click |
316
+ | `N camera push-ins in one chapter` | Push in once on the chapter's area and hold steady |
317
+ | `… only fit in the wide shot` | `frame` things that sit together, or stay wide |
318
+ | `… gives N cards; … has M` | Give the variant one entry per card of its story, in order |
319
+ | `a zoom with nothing in its shot` | Remove it, or act or read something in it |
320
+ | `zoom 2.4; use 1.3–2` | Zoom less: context reads better and stays sharp |
321
+ | `"…" looks like a testid-like name / barcode / code` | Cards are for people: say it in words |
322
+ | `"38" isn't shown by an expectText` | Quote only values an `expectText` checks |
323
+ | `the card announces a chapter in which nothing is done` | Follow it with the steps it describes |
324
+
325
+ Fix what the report names. Its `visible` list shows what is on screen at the
326
+ failing step. Repeat until it passes, at most 3 rounds; a story that still
327
+ fails is reported as "couldn't show this", with the last report. Then:
328
+
329
+ pnpm --dir <app> exec demo-video render [--story <name>] [--brand <name>]
330
+
331
+ Rendering films every frame (60 fps, frame by frame), so it takes about
332
+ five times the video's length. While tuning, `render --draft` films at
333
+ 30 fps in about half the time (`…-draft-demo.mp4`); render the final without
334
+ it. Playwright starts the app's dev server from
335
+ the adapter's `webServer`; if it can't on this machine, start it yourself
336
+ with the same command and env, and it is reused.
337
+
338
+ ## 6. Review like a viewer
339
+
340
+ Open `<app>/demo/out/review/sheet.jpg` first: one frame a second, the whole
341
+ video at a glance. Then the full-size stills beside it. Judge against
342
+ look.md:
343
+ - **Story:** each statement card is followed by exactly what it says, and
344
+ the payoff by the result it states.
345
+ - **Camera:** every push-in lands where something then happens; no shot is
346
+ mostly empty space; each click's result is on screen.
347
+ - **Framing:** pushed-in shots fill the frame and read on a phone; nothing
348
+ important is cut off; the cursor rests out of the way.
349
+ - **Pacing:** each chapter is one idea; nothing rushed, nothing lingering.
350
+ - **Truth:** every word is true of what's on screen.
351
+
352
+ Adjust the JSON and re-render. Two rounds of tuning is normal; if it takes
353
+ more, the plan from step 2 is the problem.
354
+
355
+ ## Report
356
+
357
+ Give:
358
+ - the MP4 path and duration
359
+ - the chapter plan
360
+ - the contact sheet and stills
361
+ - which brand it was rendered with, and for a customer brand, that the
362
+ video needs the customer's approval before it is published
363
+ - anything the video could not show truthfully (it shows demo fixture data,
364
+ not a real tenant's)