@plaintake/scenario 1.9.0 → 1.12.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/dist/index.js CHANGED
@@ -443,7 +443,32 @@ var HighlightRectSchema = z5.object({
443
443
  height: z5.number().int().positive(),
444
444
  startMs: z5.number().int().nonnegative(),
445
445
  endMs: z5.number().int().positive(),
446
- label: z5.string().min(1).optional()
446
+ label: z5.string().min(1).optional(),
447
+ /**
448
+ * The rect the spotlight glides *from*, when this window's cutout should animate rather
449
+ * than cut straight to the held `x`/`y`/`width`/`height` — the pre-reveal position a
450
+ * step's `target` measured before a scroll-causing `reveal` settled it into place.
451
+ * Present only together with `settleMs`: a glide needs both where it starts and when it
452
+ * finishes, and one without the other cannot be drawn.
453
+ *
454
+ * Absent means exactly today's behaviour — a single static rect held for the window's
455
+ * whole span — so every plan frozen before this field existed, and every window a future
456
+ * derivation decides not to glide, is unaffected by construction.
457
+ */
458
+ from: z5.object({
459
+ x: z5.number().int().min(0),
460
+ y: z5.number().int().min(0),
461
+ width: z5.number().int().positive(),
462
+ height: z5.number().int().positive()
463
+ }).optional(),
464
+ /**
465
+ * The absolute video-ms at which the glide from `from` completes and the spotlight
466
+ * settles into the held rect — not a duration, the same convention `startMs`/`endMs`
467
+ * and `CameraShot.enterMs`/`holdFromMs` already carry. Strictly after `startMs` (there
468
+ * has to be real time left to glide across) and no later than `endMs` (the window
469
+ * cannot settle after it has already closed).
470
+ */
471
+ settleMs: z5.number().int().positive().optional()
447
472
  }).refine(({ x, y, width, height }) => x + width <= 1920 && y + height <= 1080, {
448
473
  // Unlike CameraShotSchema's crop window, a highlight rect is drawn by libass, not fed to
449
474
  // `crop` — there is no yuv420p even-pixel rule to enforce — but it still has to fit the
@@ -451,6 +476,27 @@ var HighlightRectSchema = z5.object({
451
476
  // meant to sit inside.
452
477
  message: "the rect must stay inside the 1920x1080 frame",
453
478
  path: ["x"]
479
+ }).refine(({ from, settleMs }) => from === void 0 === (settleMs === void 0), {
480
+ // A glide needs both where it starts (`from`) and when it finishes (`settleMs`) — one
481
+ // without the other cannot be drawn, so the pair is refused rather than tolerated as a
482
+ // half-declared capability.
483
+ message: "from and settleMs must be present together or both absent",
484
+ path: ["from"]
485
+ }).refine(
486
+ ({ from }) => from === void 0 || from.x + from.width <= 1920 && from.y + from.height <= 1080,
487
+ {
488
+ // The same 1920x1080 containment the held rect enforces above, applied to the rect the
489
+ // spotlight glides from — an off-canvas starting point is no more drawable than an
490
+ // off-canvas held one.
491
+ message: "from must stay inside the 1920x1080 frame",
492
+ path: ["from"]
493
+ }
494
+ ).refine(({ startMs, endMs, settleMs }) => settleMs === void 0 || settleMs > startMs && settleMs <= endMs, {
495
+ // settleMs is absolute video-ms, like startMs/endMs themselves — the glide needs real
496
+ // room to run (strictly after startMs) and cannot settle after its own window has
497
+ // already closed (no later than endMs).
498
+ message: "settleMs must be strictly after startMs and no later than endMs",
499
+ path: ["settleMs"]
454
500
  });
455
501
  var HighlightSchema = z5.object({
456
502
  assPath: z5.literal("captions/highlight.ass"),
@@ -543,6 +589,7 @@ var SpeechSchema = z5.object({
543
589
  message: "engine must be recorded whenever any clip was synthesised",
544
590
  path: ["engine"]
545
591
  });
592
+ var ThemeSchema = z5.object({ accentColor: HEX_COLOUR });
546
593
  var RENDER_PLAN_SCHEMA = "agent-demo.render/v1";
547
594
  var RenderPlanSchema = z5.object({
548
595
  schema: z5.literal(RENDER_PLAN_SCHEMA),
@@ -730,6 +777,31 @@ var RenderPlanSchema = z5.object({
730
777
  camera: CameraSchema.optional(),
731
778
  highlight: HighlightSchema.optional(),
732
779
  speech: SpeechSchema.optional(),
780
+ /**
781
+ * The branding decision, resolved once from the licence and configuration at run time
782
+ * and frozen whole — the accent the cursor's fill, its click-ripple ring and the
783
+ * highlight label's text all draw. See `ThemeSchema`.
784
+ *
785
+ * Optional and **never defaulted**. The plan's own JSON is a weaker claim than the
786
+ * render — the schema doc above records how a Zod `.default()` materialises on parse,
787
+ * so `stableStringify` writes the field into `render/render-plan.json` and diverges the
788
+ * committed golden plan, which is what `style`'s defaults already did once. A default
789
+ * here would do it again on every parse; absent means off, and the object appears only
790
+ * when a theme was actually resolved.
791
+ *
792
+ * Top-level — a sibling of `intro`/`outro`/`cursor`/`highlight`, not a key inside one
793
+ * of them — so `recutPlan`'s `{...plan}` spread carries it through `--aspect` re-cuts
794
+ * unchanged; a nested key would need explicit handling there.
795
+ *
796
+ * The plan is not `.strict()`, so an older binary strips a `theme` it has never heard
797
+ * of — but a plain re-render is unaffected by that: `renderBundle` executes the frozen
798
+ * themed `cursor.ass`/`highlight.ass` verbatim and never regenerates them, exactly as
799
+ * an older build ignoring `speech` still muxes the frozen WAV. The hardcoded colours
800
+ * return only where derived files are *redrawn* — a fresh freeze, or a `render
801
+ * --aspect` re-cut, whose `writeCaptions(recut)` regenerates the tracks from a plan the
802
+ * older binary's own re-parse has stripped `theme` out of.
803
+ */
804
+ theme: ThemeSchema.optional(),
733
805
  /**
734
806
  * The capture timeline cut into windows — of a genuine multi-actor cast, of an implicit
735
807
  * single actor pausing for explain cut-aways, or both at once. `segments` is the one
@@ -296,9 +296,11 @@ export declare const TRANSITION_CARD_DEFAULT_MS = 3000;
296
296
  * to be a pure function of the plan, so the rect a step measured has to survive plan freeze
297
297
  * as a rect, not merely as a decision to draw one.
298
298
  *
299
- * `startMs`/`endMs` bound the whole step, not a retimed lead/rest/glide the way a cursor
300
- * point is — a spotlight has no motion to choreograph, so unlike `CursorPointSchema` there is
301
- * nothing here to pull earlier or hold open.
299
+ * `startMs`/`endMs` carry a window the derivation already retimed, the way a cursor point's
300
+ * do: a click's brackets the press — opening when the pointer parks and closing as the click
301
+ * ripple ends — while a continuous action's spans its whole step. The times are frozen as
302
+ * computed, so unlike `CursorPointSchema` (whose `rippleMs` the emitter re-derives fades
303
+ * around) there is no further choreography left for the render to do.
302
304
  *
303
305
  * `label` is the optional callout text a scenario may attach (`highlight: { label: '...' }`).
304
306
  * Absent means the spotlight alone, exactly what `highlight: true` (no object) asks for.
@@ -312,6 +314,13 @@ export declare const HighlightRectSchema: z.ZodObject<{
312
314
  startMs: z.ZodNumber;
313
315
  endMs: z.ZodNumber;
314
316
  label: z.ZodOptional<z.ZodString>;
317
+ from: z.ZodOptional<z.ZodObject<{
318
+ x: z.ZodNumber;
319
+ y: z.ZodNumber;
320
+ width: z.ZodNumber;
321
+ height: z.ZodNumber;
322
+ }, z.core.$strip>>;
323
+ settleMs: z.ZodOptional<z.ZodNumber>;
315
324
  }, z.core.$strip>;
316
325
  /**
317
326
  * The highlight track. A rect **list**, ordered and non-overlapping on the same discipline
@@ -330,6 +339,13 @@ export declare const HighlightSchema: z.ZodObject<{
330
339
  startMs: z.ZodNumber;
331
340
  endMs: z.ZodNumber;
332
341
  label: z.ZodOptional<z.ZodString>;
342
+ from: z.ZodOptional<z.ZodObject<{
343
+ x: z.ZodNumber;
344
+ y: z.ZodNumber;
345
+ width: z.ZodNumber;
346
+ height: z.ZodNumber;
347
+ }, z.core.$strip>>;
348
+ settleMs: z.ZodOptional<z.ZodNumber>;
333
349
  }, z.core.$strip>>;
334
350
  }, z.core.$strip>;
335
351
  export type Highlight = z.infer<typeof HighlightSchema>;
@@ -457,6 +473,54 @@ export type IntroInput = Omit<Intro, 'assPath'>;
457
473
  * position in the array against the schema's own `captions/turn-[0-9]+\.ass` family.
458
474
  */
459
475
  export type TransitionCardInput = Omit<TransitionCard, 'assPath'>;
476
+ /**
477
+ * The brand accent: one colour shared by the cursor's fill, its click-ripple ring and the
478
+ * highlight label's text. One field rather than one per consumer, because the cursor and
479
+ * the highlight should never disagree about the brand — the same "one block, one decision"
480
+ * shape as every other optional capability on the plan.
481
+ *
482
+ * Present only when a theme actually resolved — Pro tier, branding enabled, at least one
483
+ * colour named. Absent means every hardcoded colour the emitters already draw, so
484
+ * an unbranded run's `cursor.ass`/`highlight.ass` are exactly what they always were.
485
+ * Caption colours deliberately do not live here: `RenderPlanSchema.style` already carries
486
+ * `textColor`/`outlineColor`/`boxColor` as the single source of `captions/captions.ass`,
487
+ * and theming captions is a change to what gets frozen into those, not a second place a
488
+ * colour can hide.
489
+ *
490
+ * `accentColor` is `HEX_COLOUR` for the reason every plan colour is: the value reaches
491
+ * `assColour` and from there an ASS override tag, and `#RRGGBB` admits no ASS
492
+ * metacharacter. It is stored in the form the author wrote and converted to `&HAABBGGRR`
493
+ * only at emission, so the bundle shows the colour, not its encoding.
494
+ *
495
+ * Like the inputs above, this is the decision as the application layer supplies it — but
496
+ * it is the narrow end of a wider input: `buildRenderPlan` takes the four-field
497
+ * `ThemeInput` below and freezes only the accent here, because the caption colours have
498
+ * somewhere else to go.
499
+ */
500
+ export declare const ThemeSchema: z.ZodObject<{
501
+ accentColor: z.ZodString;
502
+ }, z.core.$strip>;
503
+ export type Theme = z.infer<typeof ThemeSchema>;
504
+ /**
505
+ * The theme decision as the application layer supplies it — all four colours a licence can
506
+ * resolve, in the same shape `@plaintake/license`'s `resolveTheme` returns, exactly as
507
+ * `OutroInput`/`IntroInput` are the shapes its other resolvers return. Only the fields the
508
+ * user actually configured appear; each absent field keeps the hardcoded default at the
509
+ * freeze, so recolouring `accentColor` alone is a complete theme.
510
+ *
511
+ * Wider than `ThemeSchema` on purpose. The plan-level block carries only `accentColor`
512
+ * because the three caption colours freeze into `RenderPlanSchema.style` — the single
513
+ * source `captions.ass` already reads — while the accent has no style field to freeze
514
+ * into and needs the plan key. A theme with no accent therefore freezes no `theme` key at
515
+ * all: the schema requires one, and defaulting it would recolour cursor surfaces the
516
+ * author never asked about.
517
+ */
518
+ export type ThemeInput = {
519
+ accentColor?: string | undefined;
520
+ captionTextColor?: string | undefined;
521
+ captionOutlineColor?: string | undefined;
522
+ captionBoxColor?: string | undefined;
523
+ };
460
524
  /**
461
525
  * The one value a plan's `schema` field ever holds.
462
526
  *
@@ -638,6 +702,13 @@ export declare const RenderPlanSchema: z.ZodObject<{
638
702
  startMs: z.ZodNumber;
639
703
  endMs: z.ZodNumber;
640
704
  label: z.ZodOptional<z.ZodString>;
705
+ from: z.ZodOptional<z.ZodObject<{
706
+ x: z.ZodNumber;
707
+ y: z.ZodNumber;
708
+ width: z.ZodNumber;
709
+ height: z.ZodNumber;
710
+ }, z.core.$strip>>;
711
+ settleMs: z.ZodOptional<z.ZodNumber>;
641
712
  }, z.core.$strip>>;
642
713
  }, z.core.$strip>>;
643
714
  speech: z.ZodOptional<z.ZodObject<{
@@ -663,6 +734,9 @@ export declare const RenderPlanSchema: z.ZodObject<{
663
734
  voice: z.ZodString;
664
735
  }, z.core.$strip>>;
665
736
  }, z.core.$strip>>;
737
+ theme: z.ZodOptional<z.ZodObject<{
738
+ accentColor: z.ZodString;
739
+ }, z.core.$strip>>;
666
740
  actors: z.ZodOptional<z.ZodArray<z.ZodObject<{
667
741
  id: z.ZodString;
668
742
  label: z.ZodString;
package/dist/types.d.ts CHANGED
@@ -49,6 +49,63 @@ export type DemoStep = {
49
49
  voice?: string;
50
50
  /** Extra presentation hold after the action completes, in whole milliseconds. */
51
51
  holdMs?: number;
52
+ /**
53
+ * Scrolled into view after `run()` completes and before the hold — the opposite ordering
54
+ * from `target`, which is measured *before* `run()`. Exists for content the action itself
55
+ * reveals below the fold (a response panel that renders after a click, say): without it,
56
+ * an author either remembers to append a separate `Actor.scroll()` step for what is
57
+ * conceptually part of this one action, or the recording holds on an off-screen result.
58
+ *
59
+ * Independent of `target`: the element acted on and the element revealed are often not
60
+ * the same one, and a step may declare `reveal` with none at all.
61
+ *
62
+ * Not best-effort like `target`'s measurement — a `reveal` locator that never appears
63
+ * fails the step normally, the same as `run()` itself or `Actor.scroll`'s own unguarded
64
+ * `scrollIntoViewIfNeeded()`, because unlike `target` (inert metadata) this performs a
65
+ * real action with a real effect on what capture sees.
66
+ *
67
+ * `target` (if declared) is re-measured, best-effort, once this scroll settles: the
68
+ * settled rect and a real "revealed at" timestamp both ride on `step.finish`, alongside
69
+ * the pre-scroll rect `step.start` already carried. Camera zoom and `highlight` both
70
+ * prefer that settled rect over the stale one once it is written, so declaring the same
71
+ * locator as both `target` and `reveal` is the supported way to have them frame the
72
+ * revealed content rather than the position it was scrolled away from —
73
+ * `Actor.scroll`'s own locator form takes the mirror-image approach of scrolling in
74
+ * `prepare`, *before* measuring, for the same reason (see its comment); `reveal` cannot
75
+ * do that since the scroll it causes is itself a consequence of `run()`. Camera zoom
76
+ * additionally anchors a shot's settle time to the *later* of the click and the reveal
77
+ * completing, rather than the click alone, so a `run()` that keeps the page busy after
78
+ * the click (rendering a response panel, say) does not leave the camera's pan finished
79
+ * before the captured frames actually show the revealed content. That push has a limit,
80
+ * though: if this step's reveal is slow AND the next step follows in quick succession,
81
+ * the settle time is pulled earlier so it cannot land after the next step's own arrival —
82
+ * preserving step order rather than exactly matching when the reveal finished, diagnosed
83
+ * with `camera.compressed` when it binds.
84
+ *
85
+ * The scroll itself is a real, live glide (`smoothScrollIntoView` in `recorder-playwright`),
86
+ * not an instant jump — a genuine rAF-driven ease over the capture's own variable-frame-rate
87
+ * screencast, the one piece of on-screen motion in a plaintake recording that is not
88
+ * synthesised at render time from frozen numbers. See that function's doc comment for why a
89
+ * live capture can show it without touching the byte-reproducibility guarantee, which has
90
+ * only ever covered re-rendering an already-frozen bundle.
91
+ *
92
+ * Two narrower caveats remain, both because they cannot be *re*-measured the way `target`
93
+ * is: (a) the cursor arrow itself still marks the pre-scroll click position and does not
94
+ * chase the settled rect, so a large reveal-scroll can leave the arrow outside a
95
+ * tightly-zoomed shot for the rest of the hold; (b) on a `click`-action step specifically,
96
+ * `highlight`'s spotlight window is a brief pulse bracketing the click, with no structural
97
+ * room for a glide to read as intentional motion rather than a timing glitch — it still
98
+ * settles on the target rect instantly, a property of that window's own timing rather than
99
+ * a gap left to close later. Non-click actions (`type`/`point`/`scroll`) are different:
100
+ * their window already spans to the step's own end, so when the pre-reveal and settled
101
+ * rects genuinely differ, the spotlight glides between them at render time — timed to when
102
+ * the reveal actually finished, not the window's own edges — falling back to the same
103
+ * instant settle when there isn't genuinely enough of that window left to glide across,
104
+ * or when the pre-reveal position itself was never usable (no area, or entirely off-frame).
105
+ *
106
+ * Also: if `run()` throws, this scroll never executes — the step fails before reaching it.
107
+ */
108
+ reveal?: Locator;
52
109
  run: () => Promise<unknown>;
53
110
  };
54
111
  export type DemoAssertion = {
@@ -304,8 +361,25 @@ export type ActorStepMeta = {
304
361
  highlight?: boolean | {
305
362
  label?: string;
306
363
  };
364
+ /** See `DemoStep.reveal`. */
365
+ reveal?: Locator;
307
366
  /** See `DemoStep.voice` — one verb-call override, ahead of this actor's own `voice`. */
308
367
  voice?: string;
368
+ /**
369
+ * Alignment `Actor.scroll`'s locator form settles the viewport on, mirroring
370
+ * `Element.scrollIntoView`'s own `block`. Only `scroll` reads this — every other verb
371
+ * has no notion of where the viewport ends up after it runs, so this is silently ignored
372
+ * elsewhere. Absent means `'center'`, the same default a bare `scrollIntoView()` call has.
373
+ */
374
+ block?: 'start' | 'center' | 'end' | 'nearest';
375
+ /**
376
+ * Minimum gap, in pixels, `Actor.scroll`'s locator form leaves between the target and the
377
+ * edge `block` settles it against — breathing room so the target doesn't land flush
378
+ * against the viewport edge. Only meaningful alongside `block: 'start'` or `'end'`, which
379
+ * name an edge; ignored for `'center'`/`'nearest'` and by every verb but `scroll`. Absent
380
+ * means 0, flush with the edge.
381
+ */
382
+ paddingPx?: number;
309
383
  };
310
384
  /**
311
385
  * A named participant in a multi-actor demo: its own Playwright `BrowserContext`, its own
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plaintake/scenario",
3
- "version": "1.9.0",
3
+ "version": "1.12.0",
4
4
  "description": "Authoring SDK for PlainTake demo scenarios: defineDemo and the scenario DSL types. Install for editor autocomplete; the PlainTake binary ships a runtime fallback.",
5
5
  "license": "MIT",
6
6
  "type": "module",