@navipartner/demo-video 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -9
- package/bin/demo-video.mjs +98 -17
- package/bin/init.d.mts +2 -0
- package/bin/init.mjs +17 -6
- package/dist/{adapter-CDOXdUZI.d.ts → adapter-DxZmb-QU.d.ts} +47 -8
- package/dist/{chunk-D7UYGNSF.js → chunk-XMOGUZJU.js} +211 -50
- package/dist/index.d.ts +119 -20
- package/dist/index.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/index.js +753 -217
- package/package.json +1 -1
- package/skills/np-demo-video/SKILL.md +242 -230
- package/skills/np-demo-video/look.md +182 -82
- package/skills/np-demo-video/reference.md +89 -30
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# @navipartner/demo-video
|
|
2
2
|
|
|
3
|
-
Marketing demo videos of React apps, in the NaviPartner look:
|
|
4
|
-
pushes and serif
|
|
3
|
+
Marketing demo videos of React apps, in the NaviPartner look: brisk camera
|
|
4
|
+
pushes and big serif cards that can show a snapshot of the result beside
|
|
5
|
+
the words. A story is a showcase of an app's main journey (45–90 s) or a
|
|
6
|
+
clip of one feature (15–30 s). The engine films an app on its own
|
|
5
7
|
Playwright e2e mocks, frame by frame at 60 fps, so pans stay smooth. It
|
|
6
8
|
works in pnpm repos on GitHub Actions, with `@playwright/test` 1.59 or later.
|
|
7
9
|
|
|
@@ -18,7 +20,8 @@ From the repo root:
|
|
|
18
20
|
|
|
19
21
|
`init` adds the skill, the `demo video` label and the workflow, and finds
|
|
20
22
|
the apps: folders with a `playwright.config`. A repo with one app is set up
|
|
21
|
-
in one go; with several,
|
|
23
|
+
in one go; with several, it prints the `init --app <folder>` line to run
|
|
24
|
+
for each.
|
|
22
25
|
It's safe to re-run, and `--dry-run` previews. Then ask Claude for a demo
|
|
23
26
|
video of the app: it writes the adapter and the primary story with you.
|
|
24
27
|
|
|
@@ -30,7 +33,7 @@ video of the app: it writes the adapter and the primary story with you.
|
|
|
30
33
|
| Linear | @Linear "… for Northwind (northwind.example)" | the same, in a customer's researched brand |
|
|
31
34
|
| A PR | the `demo video` label | reviewing the stories the PR adds or changes |
|
|
32
35
|
| A PR | `/demo-video checkout` (`help` for the menu) | filming any story from the PR's branch |
|
|
33
|
-
| Your machine |
|
|
36
|
+
| Your machine | Claude with the skill | writing and tuning a story: 30 fps drafts, then the 60 fps video once you say the whole flow is right |
|
|
34
37
|
|
|
35
38
|
Requests only pick existing stories; new stories come through a developer's
|
|
36
39
|
PR. In a PR comment, the first line is the request, a translation is found
|
|
@@ -41,8 +44,9 @@ the menu.
|
|
|
41
44
|
|
|
42
45
|
Ask @Linear in plain words, naming the repo, the story and the customer. In
|
|
43
46
|
a coding session it follows the skill's `linear.md`, attaches the MP4 and
|
|
44
|
-
its contact sheet to the issue, and commits nothing. It
|
|
45
|
-
credits; its sandbox is slow, about
|
|
47
|
+
its contact sheet to the issue, and commits nothing. It films the agreed
|
|
48
|
+
story at 60 fps. It runs on Linear's AI credits; its sandbox is slow, about
|
|
49
|
+
15× the video's length at 60 fps.
|
|
46
50
|
|
|
47
51
|
For a customer, the video is a **pitch draft**. @Linear researches their
|
|
48
52
|
brand from their own website, the engine checks it and films at 30 fps, and
|
|
@@ -72,7 +76,7 @@ Run them in the app's folder: `pnpm exec demo-video <command>`.
|
|
|
72
76
|
| `init` | sets up a repo or an app |
|
|
73
77
|
| `list` | the stories, primary first |
|
|
74
78
|
| `check` | a dry run in seconds that names what would film badly |
|
|
75
|
-
| `render` | the MP4, a contact sheet and ten stills, in `demo/out/` |
|
|
79
|
+
| `render` | the MP4, a contact sheet and ten stills, in `demo/out/` (it checks first when cards show snapshots) |
|
|
76
80
|
| `record`, `encode`, `stills` | `render` in parts |
|
|
77
81
|
| `brand-needs` | what a brand must give this app |
|
|
78
82
|
| `pitch <proposal.json>` | a pitch draft from a researched brand |
|
|
@@ -89,8 +93,8 @@ output is kept in `demo/out/failed/`.
|
|
|
89
93
|
pnpm test && pnpm typecheck && pnpm lint && pnpm build
|
|
90
94
|
|
|
91
95
|
CI runs the tests, actionlint and a smoke render of the fixture app. To
|
|
92
|
-
release,
|
|
93
|
-
|
|
96
|
+
release, tag main: `git tag vX.Y.Z && git push origin vX.Y.Z`. The tag
|
|
97
|
+
sets the version; `package.json` isn't bumped by hand. CI publishes to npm as `latest`
|
|
94
98
|
through trusted publishing (no npm token) and moves the `v0` tag. Running
|
|
95
99
|
`release.yml` by hand publishes a `next` prerelease.
|
|
96
100
|
|
package/bin/demo-video.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import path from "node:path";
|
|
|
8
8
|
|
|
9
9
|
import { listStories, parseArgs } from "./args.mjs";
|
|
10
10
|
import { encodeRecording, extractStillsFrom } from "./encode.mjs";
|
|
11
|
-
import { initApp, initProject } from "./init.mjs";
|
|
11
|
+
import { findApps, initApp, initProject } from "./init.mjs";
|
|
12
12
|
import { appMenu, menuMarkdown, pick } from "./pick.mjs";
|
|
13
13
|
import { buildPitchBrand } from "./pitch.mjs";
|
|
14
14
|
import { writeRunner } from "./runner.mjs";
|
|
@@ -56,15 +56,91 @@ const runPlaywright = (args, mode) => {
|
|
|
56
56
|
DEMO_OUT: args.outDir,
|
|
57
57
|
DEMO_WORK: args.workDir,
|
|
58
58
|
DEMO_DRAFT: args.draft ? "1" : "",
|
|
59
|
+
DEMO_STILLS: args.stillsDir ?? "",
|
|
59
60
|
},
|
|
60
61
|
},
|
|
61
62
|
);
|
|
62
63
|
return result.status ?? 1;
|
|
63
64
|
};
|
|
64
65
|
|
|
66
|
+
const readReport = (args) => {
|
|
67
|
+
const reportPath = path.join(args.workDir, "check.json");
|
|
68
|
+
return fs.existsSync(reportPath)
|
|
69
|
+
? JSON.parse(fs.readFileSync(reportPath, "utf8"))
|
|
70
|
+
: {
|
|
71
|
+
ok: false,
|
|
72
|
+
error:
|
|
73
|
+
"the dry run produced no report (Playwright did not run the demo test — see its output above)",
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// A card that shows a still of its chapter's result (`show`) needs the
|
|
78
|
+
// still before filming: a check captures it, as the element last looks in
|
|
79
|
+
// that chapter. Returns the check's run, whose folder holds the stills
|
|
80
|
+
// until the take is filmed, or null when no card shows one.
|
|
81
|
+
const captureStills = (args) => {
|
|
82
|
+
const story = JSON.parse(fs.readFileSync(args.storyboardPath, "utf8"));
|
|
83
|
+
const shows =
|
|
84
|
+
story.title?.show !== undefined ||
|
|
85
|
+
(story.steps ?? []).some((s) => s?.do === "card" && s.show !== undefined);
|
|
86
|
+
if (!shows) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const workDir = path.join(path.dirname(args.workDir), "check");
|
|
90
|
+
const runnerDir = path.join(workDir, "runner");
|
|
91
|
+
const check = {
|
|
92
|
+
...args,
|
|
93
|
+
command: "check",
|
|
94
|
+
workDir,
|
|
95
|
+
runnerDir,
|
|
96
|
+
configPath: path.join(runnerDir, "playwright.config.mjs"),
|
|
97
|
+
stillsDir: path.join(workDir, "stills"),
|
|
98
|
+
};
|
|
99
|
+
const status = runPlaywright(check, "check");
|
|
100
|
+
const report = readReport(check);
|
|
101
|
+
if (!report.ok || status !== 0) {
|
|
102
|
+
finish(check, false);
|
|
103
|
+
throw new Error(
|
|
104
|
+
`The story doesn't pass check, so its card stills can't be captured: ${report.error}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return check;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Films the take, with the card stills when the story has any.
|
|
111
|
+
const film = (args) => {
|
|
112
|
+
const check = captureStills(args);
|
|
113
|
+
const status = runPlaywright(
|
|
114
|
+
{ ...args, stillsDir: check?.stillsDir },
|
|
115
|
+
"record",
|
|
116
|
+
);
|
|
117
|
+
if (check) {
|
|
118
|
+
finish(check, true);
|
|
119
|
+
}
|
|
120
|
+
return status;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// How long each kind runs (look.md, src/timing.ts KIND_SECONDS).
|
|
124
|
+
const KIND_SECONDS = { showcase: [45, 90], feature: [15, 30] };
|
|
125
|
+
|
|
65
126
|
const printReport = (report) => {
|
|
66
127
|
if (report.ok) {
|
|
67
128
|
console.log(`✓ story passes (${report.steps} steps)`);
|
|
129
|
+
const range = KIND_SECONDS[report.kind];
|
|
130
|
+
if (report.seconds !== undefined && range) {
|
|
131
|
+
const [low, high] = range;
|
|
132
|
+
const fits =
|
|
133
|
+
report.seconds < low ? "short" : report.seconds > high ? "long" : null;
|
|
134
|
+
console.log(
|
|
135
|
+
` about ${report.seconds} s; a ${report.kind === "feature" ? "feature clip" : "showcase"} runs ${low}–${high} s${fits ? `, so this is ${fits}` : ""}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
// A card held up by its setup lingers: worth moving some of it.
|
|
139
|
+
for (const card of report.slowCards ?? []) {
|
|
140
|
+
console.log(
|
|
141
|
+
` ${card.where}: the setup behind it takes about ${card.setupSeconds} s, so it stays up longer than its ${card.readSeconds} s to read; move some setup behind an earlier card`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
68
144
|
return;
|
|
69
145
|
}
|
|
70
146
|
const where = report.step
|
|
@@ -100,6 +176,14 @@ const runInit = (argv) => {
|
|
|
100
176
|
return 2;
|
|
101
177
|
}
|
|
102
178
|
const root = process.cwd();
|
|
179
|
+
if (app && !fs.existsSync(path.join(root, app, "package.json"))) {
|
|
180
|
+
// Name the folders that are apps, so the fix is a copy away.
|
|
181
|
+
const apps = findApps(root);
|
|
182
|
+
console.error(
|
|
183
|
+
`${app} isn't an app: it has no package.json.${apps.length ? `\nApps here: ${apps.join(", ")}` : ""}`,
|
|
184
|
+
);
|
|
185
|
+
return 2;
|
|
186
|
+
}
|
|
103
187
|
const result = app
|
|
104
188
|
? initApp(path.resolve(root, app), { dryRun })
|
|
105
189
|
: initProject(root, { dryRun });
|
|
@@ -207,21 +291,25 @@ const runPitch = async (argv) => {
|
|
|
207
291
|
brandsDir,
|
|
208
292
|
"--json",
|
|
209
293
|
];
|
|
294
|
+
// The check also captures the card stills, in the customer's brand.
|
|
210
295
|
const check = parseArgs(["check", ...common], process.cwd());
|
|
296
|
+
check.stillsDir = path.join(check.workDir, "stills");
|
|
211
297
|
const checked = runPlaywright(check, "check");
|
|
212
|
-
const
|
|
213
|
-
const report = fs.existsSync(reportPath)
|
|
214
|
-
? JSON.parse(fs.readFileSync(reportPath, "utf8"))
|
|
215
|
-
: { ok: false, error: "the dry run produced no report" };
|
|
216
|
-
finish(check, report.ok && checked === 0);
|
|
298
|
+
const report = readReport(check);
|
|
217
299
|
if (!report.ok || checked !== 0) {
|
|
300
|
+
finish(check, false);
|
|
218
301
|
throw new Error(
|
|
219
302
|
`The researched brand doesn't film with this story: ${report.error}`,
|
|
220
303
|
);
|
|
221
304
|
}
|
|
222
305
|
// A pitch is a draft for one customer: 30 fps, about half the time.
|
|
223
306
|
const render = parseArgs(["render", ...common, "--draft"], process.cwd());
|
|
224
|
-
|
|
307
|
+
const filmed = runPlaywright(
|
|
308
|
+
{ ...render, stillsDir: check.stillsDir },
|
|
309
|
+
"record",
|
|
310
|
+
);
|
|
311
|
+
finish(check, true);
|
|
312
|
+
if (filmed !== 0) {
|
|
225
313
|
finish(render, false);
|
|
226
314
|
throw new Error("Filming the pitch draft failed (see above)");
|
|
227
315
|
}
|
|
@@ -279,14 +367,7 @@ const run = () => {
|
|
|
279
367
|
case "check": {
|
|
280
368
|
const status = runPlaywright(args, "check");
|
|
281
369
|
// The report comes back through the work folder and goes with it.
|
|
282
|
-
const
|
|
283
|
-
const report = fs.existsSync(reportPath)
|
|
284
|
-
? JSON.parse(fs.readFileSync(reportPath, "utf8"))
|
|
285
|
-
: {
|
|
286
|
-
ok: false,
|
|
287
|
-
error:
|
|
288
|
-
"the dry run produced no report (Playwright did not run the demo test — see its output above)",
|
|
289
|
-
};
|
|
370
|
+
const report = readReport(args);
|
|
290
371
|
const ok = report.ok && status === 0;
|
|
291
372
|
finish(args, ok);
|
|
292
373
|
if (args.json) {
|
|
@@ -298,14 +379,14 @@ const run = () => {
|
|
|
298
379
|
}
|
|
299
380
|
case "record": {
|
|
300
381
|
// The recording stays in the work folder for `encode`.
|
|
301
|
-
const status =
|
|
382
|
+
const status = film(args);
|
|
302
383
|
if (status !== 0) {
|
|
303
384
|
finish(args, false);
|
|
304
385
|
}
|
|
305
386
|
return status;
|
|
306
387
|
}
|
|
307
388
|
case "render": {
|
|
308
|
-
const status =
|
|
389
|
+
const status = film(args);
|
|
309
390
|
if (status !== 0) {
|
|
310
391
|
finish(args, false);
|
|
311
392
|
return status;
|
package/bin/init.d.mts
CHANGED
|
@@ -5,6 +5,8 @@ export interface InitResult {
|
|
|
5
5
|
}
|
|
6
6
|
export const IGNORE_LINE: string;
|
|
7
7
|
export const LABEL: string;
|
|
8
|
+
/** How init is run before the package is installed. */
|
|
9
|
+
export const INIT: string;
|
|
8
10
|
export const CALLER_WORKFLOW: string;
|
|
9
11
|
export function initRepo(
|
|
10
12
|
root: string,
|
package/bin/init.mjs
CHANGED
|
@@ -9,6 +9,8 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
// workflow. Safe to re-run; with dryRun it reports what it would change.
|
|
10
10
|
|
|
11
11
|
const PKG = "@navipartner/demo-video";
|
|
12
|
+
/** How init is run before the package is installed (the README's way). */
|
|
13
|
+
export const INIT = `npx --ignore-scripts ${PKG} init`;
|
|
12
14
|
export const IGNORE_LINE = "**/demo/out/";
|
|
13
15
|
export const LABEL = "demo video";
|
|
14
16
|
|
|
@@ -366,7 +368,7 @@ export const initApp = (appDir, { dryRun = false } = {}) => {
|
|
|
366
368
|
const todo = [];
|
|
367
369
|
const pkgFile = path.join(appDir, "package.json");
|
|
368
370
|
if (!fs.existsSync(pkgFile)) {
|
|
369
|
-
throw new Error(
|
|
371
|
+
throw new Error(`${appDir} has no package.json, so it isn't an app.`);
|
|
370
372
|
}
|
|
371
373
|
const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
|
|
372
374
|
const version = engineVersion();
|
|
@@ -396,14 +398,19 @@ export const initApp = (appDir, { dryRun = false } = {}) => {
|
|
|
396
398
|
);
|
|
397
399
|
}
|
|
398
400
|
|
|
401
|
+
const configFile = ["ts", "mts", "js", "mjs", "cts", "cjs"]
|
|
402
|
+
.map((ext) => path.join(appDir, `playwright.config.${ext}`))
|
|
403
|
+
.find((f) => fs.existsSync(f));
|
|
404
|
+
if (!configFile) {
|
|
405
|
+
todo.push(
|
|
406
|
+
"Add a playwright.config to this folder: videos run on the app's e2e setup.",
|
|
407
|
+
);
|
|
408
|
+
}
|
|
399
409
|
const demoDir = path.join(appDir, "demo");
|
|
400
410
|
const adapter = path.join(demoDir, "demo.config.ts");
|
|
401
411
|
if (fs.existsSync(adapter)) {
|
|
402
412
|
changes.push(["ok", "demo/demo.config.ts", "exists"]);
|
|
403
413
|
} else {
|
|
404
|
-
const configFile = ["ts", "mts", "js", "mjs"]
|
|
405
|
-
.map((ext) => path.join(appDir, `playwright.config.${ext}`))
|
|
406
|
-
.find((f) => fs.existsSync(f));
|
|
407
414
|
const webServer = configFile ? webServerOf(read(configFile)) : {};
|
|
408
415
|
const skeleton = adapterSkeleton({
|
|
409
416
|
app: (pkg.name ?? path.basename(appDir)).replace(/^@[^/]+\//, ""),
|
|
@@ -492,12 +499,16 @@ export const initProject = (root, options = {}) => {
|
|
|
492
499
|
}
|
|
493
500
|
const todo = [...repo.todo];
|
|
494
501
|
if (apps.length > 1) {
|
|
502
|
+
// Exactly what to run: one line per app, copied as is.
|
|
495
503
|
todo.push(
|
|
496
|
-
|
|
504
|
+
[
|
|
505
|
+
`${apps.length} apps found and none set up yet. Run this for each app you want videos of:`,
|
|
506
|
+
...apps.map((app) => ` ${INIT} --app ${app}`),
|
|
507
|
+
].join("\n"),
|
|
497
508
|
);
|
|
498
509
|
} else {
|
|
499
510
|
todo.push(
|
|
500
|
-
|
|
511
|
+
`No app found (a folder with package.json and playwright.config). Run ${INIT} --app <folder> once it has both.`,
|
|
501
512
|
);
|
|
502
513
|
}
|
|
503
514
|
return { apps, changes: repo.changes, todo };
|
|
@@ -38,6 +38,13 @@ interface VideoBrand {
|
|
|
38
38
|
fontsUrl: string;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* What a story is (look.md): a showcase walks the app's main journey for a
|
|
43
|
+
* buyer (45–90 s); a feature clip shows one feature (15–30 s). The beats are
|
|
44
|
+
* the same; the shape differs.
|
|
45
|
+
*/
|
|
46
|
+
type Kind = "showcase" | "feature";
|
|
47
|
+
|
|
41
48
|
/** A testid (letters, digits, dash, underscore) or a Playwright selector. */
|
|
42
49
|
interface Target {
|
|
43
50
|
target: string;
|
|
@@ -53,6 +60,21 @@ interface CardSpec {
|
|
|
53
60
|
logo?: boolean;
|
|
54
61
|
style: "pale" | "brand";
|
|
55
62
|
seconds: number;
|
|
63
|
+
/** A still of what the chapter achieves, beside the words (a URL). */
|
|
64
|
+
still?: string;
|
|
65
|
+
}
|
|
66
|
+
interface CardOptions {
|
|
67
|
+
/** The next card replaces this one directly, with no app in between. */
|
|
68
|
+
keepUp?: boolean;
|
|
69
|
+
/** The video's open card: already up on the first frame, no dissolve in. */
|
|
70
|
+
opening?: boolean;
|
|
71
|
+
/** The story's kind: how long the open card may stay up. */
|
|
72
|
+
kind?: Kind;
|
|
73
|
+
/**
|
|
74
|
+
* Done while the card covers the app (setup): no cursor, no camera
|
|
75
|
+
* moves, and the card stays up until it's done.
|
|
76
|
+
*/
|
|
77
|
+
behind?: () => Promise<void>;
|
|
56
78
|
}
|
|
57
79
|
interface DirectorApi {
|
|
58
80
|
/** The app's iframe, for actions that talk to the app directly. */
|
|
@@ -75,7 +97,10 @@ interface DirectorApi {
|
|
|
75
97
|
click(target: Target, options?: {
|
|
76
98
|
zoom?: number;
|
|
77
99
|
}): Promise<void>;
|
|
78
|
-
|
|
100
|
+
/** `replace`: select what the field holds first, so the text replaces it. */
|
|
101
|
+
type(target: Target, text: string, perCharMs?: number, options?: {
|
|
102
|
+
replace?: boolean;
|
|
103
|
+
}): Promise<void>;
|
|
79
104
|
press(key: string): Promise<void>;
|
|
80
105
|
park(): Promise<void>;
|
|
81
106
|
waitFor(target: Target, options: {
|
|
@@ -91,19 +116,26 @@ interface DirectorApi {
|
|
|
91
116
|
* Runs an adapter action. While filming, time is stopped, so an action that
|
|
92
117
|
* waits on the app (a scan that fetches) needs frames filmed meanwhile.
|
|
93
118
|
*/
|
|
94
|
-
during<T>(work: () => Promise<T
|
|
119
|
+
during<T>(work: () => Promise<T>, options?: {
|
|
120
|
+
/** The control the work uses: the cursor goes there, as for a click. */
|
|
121
|
+
target?: Target;
|
|
122
|
+
}): Promise<T>;
|
|
95
123
|
/**
|
|
96
124
|
* The app's clock, in ms: real time in check mode, the filmed (virtual)
|
|
97
125
|
* time while filming. The idle accounting measures waits with it.
|
|
98
126
|
*/
|
|
99
127
|
clockMs(): number;
|
|
100
128
|
/**
|
|
101
|
-
* Dissolves the card in over the app and holds it
|
|
102
|
-
* unless `keepUp`: then the next
|
|
129
|
+
* Dissolves the card in over the app and holds it, doing `behind` while
|
|
130
|
+
* it covers the app. It dissolves back out unless `keepUp`: then the next
|
|
131
|
+
* card replaces it directly.
|
|
103
132
|
*/
|
|
104
|
-
card(spec: CardSpec, options?:
|
|
105
|
-
|
|
106
|
-
|
|
133
|
+
card(spec: CardSpec, options?: CardOptions): Promise<void>;
|
|
134
|
+
/**
|
|
135
|
+
* Saves a still of the element as it looks now, for a card that shows it.
|
|
136
|
+
* False when it isn't on screen.
|
|
137
|
+
*/
|
|
138
|
+
capture(target: Target, file: string): Promise<boolean>;
|
|
107
139
|
/** Testids currently visible in the app, for failure reports. */
|
|
108
140
|
visibleTestIds(): Promise<string[]>;
|
|
109
141
|
}
|
|
@@ -111,6 +143,13 @@ interface DirectorApi {
|
|
|
111
143
|
interface DemoAction<S extends z.ZodRawShape = z.ZodRawShape> {
|
|
112
144
|
params: z.ZodObject<S>;
|
|
113
145
|
run(d: DirectorApi, params: z.infer<z.ZodObject<S>>): Promise<void>;
|
|
146
|
+
/**
|
|
147
|
+
* The control the action uses (a colour field, an upload button), as a
|
|
148
|
+
* testid, a selector or `{ target, nth }`. With one, the cursor glides to
|
|
149
|
+
* it and rests before the action, the camera rules apply as for a click,
|
|
150
|
+
* and the app's reaction plays out after it.
|
|
151
|
+
*/
|
|
152
|
+
target?(params: z.infer<z.ZodObject<S>>): string | Target | undefined;
|
|
114
153
|
}
|
|
115
154
|
/** One thing a brand gives the app: what it is and where it shows. */
|
|
116
155
|
type BrandNeed = string | {
|
|
@@ -166,4 +205,4 @@ interface DemoAdapter {
|
|
|
166
205
|
declare const defineDemo: (adapter: DemoAdapter) => DemoAdapter;
|
|
167
206
|
declare const defineAction: <S extends z.ZodRawShape>(action: DemoAction<S>) => DemoAction<S>;
|
|
168
207
|
|
|
169
|
-
export { type Brand as B, type CardSpec as C, type DemoAdapter as D, type Target as T, type VideoBrand as V, type DemoAction as a, type DirectorApi as b, defineDemo as c, defineAction as d };
|
|
208
|
+
export { type Brand as B, type CardSpec as C, type DemoAdapter as D, type Kind as K, type Target as T, type VideoBrand as V, type DemoAction as a, type DirectorApi as b, defineDemo as c, defineAction as d };
|