@navipartner/demo-video 0.0.0-stage → 0.4.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/README.md +109 -3
- package/bin/args.d.mts +30 -0
- package/bin/args.mjs +153 -0
- package/bin/demo-video.mjs +272 -0
- package/bin/encode.mjs +187 -0
- package/bin/init.d.mts +24 -0
- package/bin/init.mjs +447 -0
- package/bin/pick.d.mts +18 -0
- package/bin/pick.mjs +252 -0
- package/bin/runner.d.mts +5 -0
- package/bin/runner.mjs +47 -0
- package/brands/navipartner/logo.svg +19 -0
- package/brands/navipartner.json +22 -0
- package/dist/adapter-CWqMoNdw.d.ts +157 -0
- package/dist/chunk-BYAFDWPA.js +768 -0
- package/dist/index.d.ts +255 -0
- package/dist/index.js +56 -0
- package/dist/runner/index.d.ts +23 -0
- package/dist/runner/index.js +1258 -0
- package/package.json +67 -3
- package/skills/np-demo-video/SKILL.md +348 -0
- package/skills/np-demo-video/look.md +158 -0
- package/skills/np-demo-video/reference.md +82 -0
package/package.json
CHANGED
|
@@ -1,6 +1,70 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navipartner/demo-video",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Marketing demo videos of a React app, filmed by Playwright against the app's own e2e mocks.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/navipartner/demo-video.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=22"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./runner": {
|
|
22
|
+
"types": "./dist/runner/index.d.ts",
|
|
23
|
+
"import": "./dist/runner/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"demo-video": "./bin/demo-video.mjs"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"bin",
|
|
32
|
+
"brands",
|
|
33
|
+
"skills",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"registry": "https://registry.npmjs.org/",
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"ffmpeg-static": "5.2.0",
|
|
42
|
+
"zod": "3.25.76"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@playwright/test": ">=1.59.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@eslint/js": "^9.39.5",
|
|
49
|
+
"@playwright/test": "^1.59.0",
|
|
50
|
+
"@types/node": "^25.2.0",
|
|
51
|
+
"eslint": "^9.39.5",
|
|
52
|
+
"eslint-config-prettier": "^10.1.8",
|
|
53
|
+
"eslint-plugin-simple-import-sort": "^14.0.0",
|
|
54
|
+
"globals": "^17.11.0",
|
|
55
|
+
"prettier": "3.9.6",
|
|
56
|
+
"tsup": "^8.5.0",
|
|
57
|
+
"typescript": "^5.9.3",
|
|
58
|
+
"typescript-eslint": "^8.68.0",
|
|
59
|
+
"vitest": "^2.1.9"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "tsup",
|
|
63
|
+
"typecheck": "tsc --noEmit",
|
|
64
|
+
"test": "vitest run",
|
|
65
|
+
"test:watch": "vitest",
|
|
66
|
+
"lint": "eslint .",
|
|
67
|
+
"format": "prettier --write .",
|
|
68
|
+
"format:check": "prettier --check ."
|
|
69
|
+
}
|
|
6
70
|
}
|
|
@@ -0,0 +1,348 @@
|
|
|
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)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# The look
|
|
2
|
+
|
|
3
|
+
Every NaviPartner demo video should look and feel the same, in order of importance for the visuals:
|
|
4
|
+
|
|
5
|
+
1. **The product tells the story; text only frames it.** No caption pills
|
|
6
|
+
over the UI. Words appear on their own cards *between* the chapters, never
|
|
7
|
+
on top of the app.
|
|
8
|
+
2. **Calm, purposeful camera.** It moves between shots, never during the
|
|
9
|
+
action: one smooth push-in on the area where the chapter's action and its
|
|
10
|
+
result happen, held steady through the clicks, then back out. It never
|
|
11
|
+
chases the cursor, and there are no hard cuts inside a chapter.
|
|
12
|
+
3. **Soft, light, branded.** A pale brand-tinted background, a floating app
|
|
13
|
+
window, serif statements, and one brand colour used boldly once or twice.
|
|
14
|
+
4. **Real-looking data.** Believable names, orders and products in the
|
|
15
|
+
customer's language and market. Never "Test item 1".
|
|
16
|
+
|
|
17
|
+
## Structure
|
|
18
|
+
|
|
19
|
+
A video is a sequence of **chapters**. Each chapter is one idea the viewer
|
|
20
|
+
should remember.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
open card 2–4 s eyebrow "Introducing" + wordmark/logo; tagline fades in
|
|
24
|
+
┌ statement 2–3 s "A customer *scans* their basket."
|
|
25
|
+
│ demo 10–25 s the app doing exactly that, camera following
|
|
26
|
+
└ payoff 3–5 s optional: what just happened, as a fact
|
|
27
|
+
… 2–4 chapters …
|
|
28
|
+
tagline 2–3 s one line on brand colour: "Checkout that runs itself."
|
|
29
|
+
end card 3–5 s logo + tagline, still
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- 30 s is enough for one chapter, 60–95 s for three or four. Length follows
|
|
33
|
+
the chapters. Don't pad, don't cram.
|
|
34
|
+
- The first chapter's statement lands by second 6. The hook is the
|
|
35
|
+
open card plus the first statement, and nothing else.
|
|
36
|
+
- A chapter ends wide (the whole window), so the next card has something to
|
|
37
|
+
dissolve from.
|
|
38
|
+
|
|
39
|
+
## Text cards
|
|
40
|
+
|
|
41
|
+
| Card | Text | Look |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| Open | eyebrow `Introducing` (small sans, grey) · brand wordmark/logo · tagline | pale background, centred |
|
|
44
|
+
| Statement | ≤ 7 words naming what the next chapter shows, sentence case, ends with a full stop, **one word or phrase in italic** (the action or its result): "Pay *by card*." | dark serif on pale background |
|
|
45
|
+
| Payoff | the result just shown as heading; sub-line states the concrete fact the app showed: "64,50 kr. by card." | white serif on full-bleed brand colour |
|
|
46
|
+
| Stat | a number + what it counts: "60+ integrations" | pale, optional logo grid |
|
|
47
|
+
| Tagline | the product's promise in ≤ 5 words | brand colour or pale, alternating |
|
|
48
|
+
| End | logo + one-line tagline | pale, held still |
|
|
49
|
+
|
|
50
|
+
- **Typeface:** the brand's fonts. For NaviPartner that's **Merriweather** for
|
|
51
|
+
headings (its italic is the emphasis word) and **Work Sans** for eyebrows and
|
|
52
|
+
sub-lines.
|
|
53
|
+
At 1080p: logo ~110 px tall equivalent, statements ~70 px, sub-lines
|
|
54
|
+
~18–22 px.
|
|
55
|
+
- **Tone:** plain, confident, present tense. No exclamation marks except
|
|
56
|
+
inside a quoted customer line. No "seamless", "revolutionary" or
|
|
57
|
+
"effortless".
|
|
58
|
+
- **Truth:** every fact on a card must be visible in the video itself (the
|
|
59
|
+
price shown, the receipt that appears), checked by an `expectText`. No
|
|
60
|
+
invented durations or customer statistics. Stat cards only state real
|
|
61
|
+
product facts, such as the number of payment methods the app supports.
|
|
62
|
+
- **Never on a card:** testids, barcodes, internal names or any other code.
|
|
63
|
+
- **Reading time:** a card stays up at least 1 s plus ~0.3 s a word.
|
|
64
|
+
|
|
65
|
+
## Transitions
|
|
66
|
+
|
|
67
|
+
- **Blur dissolve** everywhere. Text blurs in from ~12 px to sharp over
|
|
68
|
+
~500 ms. A card hands over to the app by blurring out while the app blurs in
|
|
69
|
+
behind it (~700 ms).
|
|
70
|
+
- Into a payoff card: the brand colour washes over the wide app (~600 ms),
|
|
71
|
+
then the heading blurs in.
|
|
72
|
+
- Card to card: the old words blur out first (~300 ms), then the new ones
|
|
73
|
+
blur in, so two headings never show at once.
|
|
74
|
+
- No slides, wipes, spins, or zoom-punch transitions.
|
|
75
|
+
|
|
76
|
+
## Camera
|
|
77
|
+
|
|
78
|
+
- **Wide shot:** the app floats in the middle of the frame at ~90 % width.
|
|
79
|
+
Small corner radius, soft long shadow, no browser chrome.
|
|
80
|
+
- **Push in once per shot** on the area that holds the control *and* what it
|
|
81
|
+
changes, to scale **1.4–1.8** (never past 2: tighter loses the context and
|
|
82
|
+
softens the picture). The UI should fill the frame and read on a phone.
|
|
83
|
+
- **Hold steady through the action.** The clicks happen inside the shot; the
|
|
84
|
+
camera doesn't move with them. At most two push-ins a chapter.
|
|
85
|
+
- **Show the result.** When an action changes something outside the shot (a
|
|
86
|
+
row added at the top, a new screen), pull out right after it. A push-in
|
|
87
|
+
that shows a button pressed and nothing happening is the worst shot.
|
|
88
|
+
- **Each move eases in and out** (engine: 1200 ms,
|
|
89
|
+
`cubic-bezier(0.65, 0, 0.35, 1)`), then settles 300 ms before anything
|
|
90
|
+
happens. Hold ≥ 1.5 s on anything the viewer must read.
|
|
91
|
+
- Every chapter opens wide: the engine resets the camera behind each card.
|
|
92
|
+
|
|
93
|
+
## Cursor
|
|
94
|
+
|
|
95
|
+
- A small standard arrow, used sparingly. It appears for the clicks and drags
|
|
96
|
+
a person would make. It glides on a gentle curve, easing in and out
|
|
97
|
+
(550–1100 ms by distance), and doesn't pulse, ripple or jerk.
|
|
98
|
+
- **A beat around every click:** it rests on the control (220 ms, its hover
|
|
99
|
+
state shows), presses (110 ms, the pressed state shows), and the app's
|
|
100
|
+
reaction plays (650 ms) before anything else happens.
|
|
101
|
+
- Between actions it rests where it doesn't cover what the viewer should
|
|
102
|
+
read.
|
|
103
|
+
- If the UI animates by itself (an order total updating, a status changing),
|
|
104
|
+
let it play. Don't point at it.
|
|
105
|
+
|
|
106
|
+
## Background and branding
|
|
107
|
+
|
|
108
|
+
- **Background:** white tinted ~5–8 % with the brand's primary colour, with
|
|
109
|
+
one or two very soft radial blooms. Never a busy wallpaper, and never
|
|
110
|
+
dark mode.
|
|
111
|
+
- **Brand colour:** the full-bleed payoff and tagline cards are the brand
|
|
112
|
+
colour. The brand's logo and wordmark are on the open and end cards. The
|
|
113
|
+
app itself is shown in the customer's branding (their kiosk looks like
|
|
114
|
+
their kiosk).
|
|
115
|
+
- **Default brand:** NaviPartner (below). A customer video, made with the
|
|
116
|
+
customer's permission and their own brand material (see "A new brand" in
|
|
117
|
+
SKILL.md), swaps the logo, colours, fonts, tagline and data (their
|
|
118
|
+
products, their language), following the same roles.
|
|
119
|
+
|
|
120
|
+
### NaviPartner brand
|
|
121
|
+
|
|
122
|
+
These values are taken from navipartner.com and the official logo
|
|
123
|
+
(`NaviPartner_rgb.svg`) on 2026-10-02. Use exactly these, never close guesses.
|
|
124
|
+
|
|
125
|
+
| Role | Value |
|
|
126
|
+
|---|---|
|
|
127
|
+
| Primary: full-bleed payoff and tagline cards, italic emphasis word, background tint | `#1F5494` |
|
|
128
|
+
| Navy: logo wordmark, statement headings on pale cards | `#123A7E` |
|
|
129
|
+
| Light blue: the logo's "NAVI" mark, soft background bloom, sparingly | `#729FD3` |
|
|
130
|
+
| Text: sub-lines and eyebrows | `#232323`; muted `#707070` |
|
|
131
|
+
| Pale background base | `#F8F8F8` / `#FFFFFF` |
|
|
132
|
+
| Heading font | Merriweather 400/700, italic for the emphasis word |
|
|
133
|
+
| Body font | Work Sans 400/600/700 |
|
|
134
|
+
| Logo | official two-colour SVG: `#729FD3` mark + `#123A7E` wordmark, on pale backgrounds only. On `#1F5494`, use it all white. |
|
|
135
|
+
|
|
136
|
+
A NaviPartner pale card is `#F8F8F8` with a `#1F5494` tint at ~5 % and one
|
|
137
|
+
`#729FD3` bloom, with the heading in navy and the italic word in primary. A
|
|
138
|
+
brand card is full-bleed `#1F5494` with the heading in white Merriweather and
|
|
139
|
+
the sub-line in white Work Sans at 80 % opacity.
|
|
140
|
+
|
|
141
|
+
Keep the two brand layers apart. The cards and background use the video's
|
|
142
|
+
brand (NaviPartner, or the customer's). What's inside the app window is the
|
|
143
|
+
kiosk's tenant branding from the adapter's brand file.
|
|
144
|
+
|
|
145
|
+
## Sound
|
|
146
|
+
|
|
147
|
+
No music bed yet: videos are silent and must work on mute. The cards carry
|
|
148
|
+
the whole story.
|
|
149
|
+
|
|
150
|
+
## Formats
|
|
151
|
+
|
|
152
|
+
- **Master:** 1920×1080, 16:9, 60 fps H.264, filmed frame by frame (each
|
|
153
|
+
frame a sharp screenshot of exactly that moment, never a screen recording).
|
|
154
|
+
This is used for the website and the LinkedIn feed.
|
|
155
|
+
- **Portrait kiosk apps** (food order 1080×1920) float centred in the 16:9
|
|
156
|
+
frame, and the camera pushes in the same way.
|
|
157
|
+
- 1:1 / 9:16 cuts for SoMe come later. Keep card text inside the central
|
|
158
|
+
1080×1080 area so a square crop still works.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Reference: adapter, story and brand fields
|
|
2
|
+
|
|
3
|
+
## Adapter (`<app>/demo/demo.config.ts`)
|
|
4
|
+
|
|
5
|
+
`webServer` (`command`, `url`, `env`), `appPath`, `ready` (a testid shown once
|
|
6
|
+
the mocks booted the app), `viewport` (default 1728×972; a portrait kiosk is
|
|
7
|
+
scaled to fit) and `setup(page, { brand })`, which runs the app's e2e mocks
|
|
8
|
+
**unchanged** and lays the brand over the response that carries it.
|
|
9
|
+
Optional: `actions` (app steps such as a barcode scan), `idleTimeoutSeconds`,
|
|
10
|
+
`defaultBrand`, `brands` (folder, default `./brands`), `locale`, `timezone`.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { defineDemo, overlayResponse } from "@navipartner/demo-video";
|
|
14
|
+
|
|
15
|
+
import { ApiMocks } from "../e2e/helpers/apiMocks";
|
|
16
|
+
|
|
17
|
+
export default defineDemo({
|
|
18
|
+
app: "self-service-food-order",
|
|
19
|
+
webServer: { command: "pnpm dev", url: "http://localhost:3003", env: { VITE_E2E: "true" } },
|
|
20
|
+
appPath: "/?tenant=test",
|
|
21
|
+
viewport: { width: 1080, height: 1920 },
|
|
22
|
+
ready: "home-page",
|
|
23
|
+
async setup(page, { brand }) {
|
|
24
|
+
await new ApiMocks(page).setupMocks();
|
|
25
|
+
await overlayResponse(page, {
|
|
26
|
+
path: "/api/[^/]+/restaurant/?$",
|
|
27
|
+
at: "restaurants.*",
|
|
28
|
+
values: { ...brand.config, name: brand.tenantName, logoUrl: brand.assetUrls.logo },
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Story (`<app>/demo/stories/<name>.json`)
|
|
35
|
+
|
|
36
|
+
`app`, `description`, optional `primary`, `title`, `closing`, `steps`.
|
|
37
|
+
|
|
38
|
+
- **Steps:** `click` `type` `press` `zoom` `frame` `zoomOut` `park` `waitFor`
|
|
39
|
+
`expectText` `hold` `card`, plus the adapter's actions as
|
|
40
|
+
`{ "do": "<action>", …params }`. `target` is a testid or any Playwright
|
|
41
|
+
selector (`role=button[name=/pay/i]`); `nth` picks among matches (-1 =
|
|
42
|
+
last). Clicks wait until their target stops moving.
|
|
43
|
+
- **Cards** (`card`, `title`, `closing`): `eyebrow` (≤ 4 words), `heading`
|
|
44
|
+
(≤ 8, `*word*` in italic, once), `sub` (≤ 16), `logo`, `style` (`pale` |
|
|
45
|
+
`brand`), `seconds` (a minimum: a card stays up long enough to read).
|
|
46
|
+
Cards that follow each other hand over directly. A number on a card must be
|
|
47
|
+
checked by an `expectText` in the story.
|
|
48
|
+
- **Camera:** `zoom` (`target`, `scale` 1.3–2) pushes in on that element's
|
|
49
|
+
area; `frame` (`targets`: 2–4 testids or `{ target, nth }`, optional
|
|
50
|
+
`maxScale` 1.3–2, default 1.8) pushes in as far as all of them fit, and
|
|
51
|
+
fails if they only fit wide; `zoomOut` (`park`) pulls back. A `click` keeps the camera unless it
|
|
52
|
+
has its own `"zoom"`. At most two push-ins a chapter, and a zoom needs
|
|
53
|
+
something done or read in its shot. A click target must be inside the
|
|
54
|
+
current shot, and what a zoomed click changes must be in the shot or shown
|
|
55
|
+
by the next step (a `zoomOut`, or a zoom that frames it). Every chapter
|
|
56
|
+
opens wide.
|
|
57
|
+
- **Timing:** `hold` ≤ 6 s; `type` `perCharMs` ≥ 40. The engine adds the
|
|
58
|
+
beats around clicks, the camera's settle and a linger before each card.
|
|
59
|
+
Holds may not add up past the app's idle timeout.
|
|
60
|
+
|
|
61
|
+
## Variant (`<app>/demo/stories/<story>.<brand>.json`)
|
|
62
|
+
|
|
63
|
+
`extends` (the base story's name), `brand`, optional `description`, `title`,
|
|
64
|
+
`closing` and `cards` (one entry per card step of the base, in order). Each
|
|
65
|
+
card entry takes the card fields and changes only those it names. No
|
|
66
|
+
`steps`, no `primary`: the flow is the base's. Filmed with its brand only.
|
|
67
|
+
|
|
68
|
+
## Brand (`<app>/demo/brands/<name>.json`, assets in `<name>/`)
|
|
69
|
+
|
|
70
|
+
- `tenantName`
|
|
71
|
+
- `config`: opaque to the engine, the keys the app's branding API reads
|
|
72
|
+
(`primaryColor`, …). An adapter may also read `config.demoData`: the
|
|
73
|
+
customer's names for the mock's products, keyed by the mock's barcodes
|
|
74
|
+
(prices and flows stay the mock's). It takes `demoData` out before it
|
|
75
|
+
overlays the branding.
|
|
76
|
+
- `assets`: slot → file, e.g. `{ "logo": "<name>/logo.svg" }`
|
|
77
|
+
- `video`: `primary`, `ink`, `accent`, `text`, `headingFont`, `bodyFont`,
|
|
78
|
+
optional `fontsUrl`, `logo` (asset slot)
|
|
79
|
+
- optional `logoColor` (recolours white fills in an SVG logo) and
|
|
80
|
+
`stage.background`
|
|
81
|
+
|
|
82
|
+
The engine ships `navipartner`; an app's own brand of the same name wins.
|