pro-visu 0.2.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.
@@ -0,0 +1,818 @@
1
+ import { z } from 'zod';
2
+
3
+ declare const easingSchema: z.ZodEnum<["linear", "easeInOutCubic", "easeInOutQuad", "easeOutCubic", "easeInOutSine", "easeInOutExpo", "easeOutQuint"]>;
4
+ type Easing = z.infer<typeof easingSchema>;
5
+ /** One choreographed scroll step. */
6
+ interface ChoreographyStepInput {
7
+ /** Target: a 0..1 number, an "NN%" string, or a CSS selector to bring into view. */
8
+ to: number | string;
9
+ /** Travel time to this target (ms). Default 1200. */
10
+ durationMs?: number;
11
+ /** Hold time at this target after arriving (ms). Default 800. */
12
+ holdMs?: number;
13
+ /** Easing for the travel to this target. Default "easeInOutCubic". */
14
+ easing?: Easing;
15
+ }
16
+ /** One step of a scripted interaction (`actions` / `focus.actions`). */
17
+ interface InteractionActionInput {
18
+ /** What this step does. */
19
+ do: "move" | "click" | "hover" | "type" | "scrollTo" | "wait";
20
+ /** Target element for move/click/hover/type. */
21
+ selector?: string;
22
+ /** Viewport-relative X target for a selector-less `move` (0..1). */
23
+ x?: number;
24
+ /** Viewport-relative Y target for a selector-less `move` (0..1). */
25
+ y?: number;
26
+ /** Text to type (for `type`). */
27
+ text?: string;
28
+ /** Scroll target for `scrollTo`: a 0..1 number, an "NN%" string, or a CSS selector. */
29
+ to?: number | string;
30
+ /** Cursor travel / scroll animation time (ms). Default 700. */
31
+ durationMs?: number;
32
+ /** Pause after the step (ms). Default 600. */
33
+ holdMs?: number;
34
+ }
35
+ /** Tuning for auto-section choreography (`autoSections`). */
36
+ interface AutoSectionsInput {
37
+ /** Min element height (as a fraction of the viewport) to count as a section. Default 0.5. */
38
+ minHeightFraction?: number;
39
+ /** Explicit section selector; overrides the heuristic. Omit to auto-detect. */
40
+ selector?: string;
41
+ /** Hold at each detected section (ms). Default 700. */
42
+ holdMs?: number;
43
+ /** Total clip length (ms) split across detected sections. Default 12000. */
44
+ durationMs?: number;
45
+ /** Cap on the number of sections. Default 8. */
46
+ maxSections?: number;
47
+ /** Distribute travel time by distance for uniform scroll speed. Default true. */
48
+ constantVelocity?: boolean;
49
+ }
50
+ /** An intro / outro card (`intro` / `outro`). */
51
+ interface CardInput {
52
+ /** Card title (large). Omit for none. */
53
+ title?: string;
54
+ /** Card subtitle (small, under the title). Omit for none. */
55
+ subtitle?: string;
56
+ /** Card background color. Default black. */
57
+ background?: string;
58
+ /** Card text color. Default white. */
59
+ color?: string;
60
+ /** How long the card holds (ms). Default 1500. */
61
+ durationMs?: number;
62
+ /** Fade in/out length (ms). Default 400. */
63
+ fadeMs?: number;
64
+ }
65
+ /** A timed on-screen annotation (`annotations`). */
66
+ interface AnnotationInput {
67
+ /** Caption text shown while active. Omit for a ring/spotlight with no caption. */
68
+ text?: string;
69
+ /** Selector to outline with a highlight ring. */
70
+ ring?: string;
71
+ /** Selector to spotlight (everything else dimmed). */
72
+ spotlight?: string;
73
+ /** When it appears (clip time, ms). Default 0. */
74
+ atMs?: number;
75
+ /** When it disappears (clip time, ms). Default = end of clip. */
76
+ untilMs?: number;
77
+ /** Caption placement. Default "bottom". */
78
+ position?: "top" | "bottom" | "center";
79
+ }
80
+ /** Ken Burns slow-zoom config ("frames" capture only). */
81
+ interface KenBurnsInput {
82
+ /** Start scale (1 = no zoom). Default 1. */
83
+ scaleFrom?: number;
84
+ /** End scale. Default 1.08. */
85
+ scaleTo?: number;
86
+ /** Easing for the zoom ramp. Default "easeInOutCubic". */
87
+ easing?: Easing;
88
+ /** Zoom origin X within the viewport (0 = left, 1 = right). Default 0.5. */
89
+ originX?: number;
90
+ /** Zoom origin Y within the viewport (0 = top, 1 = bottom). Default 0.5. */
91
+ originY?: number;
92
+ }
93
+ /** The synthetic cursor shown during an interaction. */
94
+ interface CursorInput {
95
+ /** Show the cursor. Default true (when `actions` is set). */
96
+ show?: boolean;
97
+ /** Cursor size (px). Default 22. */
98
+ size?: number;
99
+ /** Cursor color. Default white-with-shadow. */
100
+ color?: string;
101
+ }
102
+ /** Element-focused clip config (`focus`). */
103
+ interface FocusInput {
104
+ /** Selector of the element to scroll into view and crop to. */
105
+ selector: string;
106
+ /** Padding (px) around the element when cropping. Default 24. */
107
+ padding?: number;
108
+ /** Optional steps to trigger the component (e.g. open a dropdown) before holding. */
109
+ actions?: InteractionActionInput[];
110
+ /** Time to dwell on the element after positioning / triggering (ms). Default 2000. */
111
+ holdMs?: number;
112
+ }
113
+ /** One extra viewport to also capture the reel at (`viewports`). */
114
+ interface ViewportInput {
115
+ /** Name appended to the asset (<name>-<viewport name>). */
116
+ name: string;
117
+ /** Viewport width in CSS px. */
118
+ width: number;
119
+ /** Viewport height in CSS px. */
120
+ height: number;
121
+ /** Override the generator-level `deviceScaleFactor` for this viewport. Omit to inherit it. */
122
+ deviceScaleFactor?: number;
123
+ }
124
+ /** One route in a multi-page tour: a URL string, or an object with per-route choreography. */
125
+ type RouteInput = string | {
126
+ /** Route URL (absolute, or a "/path" against the managed server). */
127
+ url: string;
128
+ /** Per-route choreographed scroll (overrides the tour default). */
129
+ choreography?: ChoreographyStepInput[];
130
+ /** Per-route auto-section choreography. */
131
+ autoSections?: boolean | AutoSectionsInput;
132
+ /** This route's slice of the tour (ms). */
133
+ durationMs?: number;
134
+ };
135
+ /** Target output aspect: a preset, or an explicit pixel box. */
136
+ type AspectInput = "16:9" | "9:16" | "1:1" | {
137
+ width: number;
138
+ height: number;
139
+ };
140
+ /**
141
+ * Author-facing options for the `scroll-reel` generator — a video of a page. By default it eases a
142
+ * single top→bottom scroll; the options below switch it into choreographed / auto-section / focus /
143
+ * interaction / multi-route modes, add cards & annotations, clean up the page, and reframe / re-encode
144
+ * the output. Everything is optional, with the defaults noted below.
145
+ */
146
+ interface ScrollReelOptionsInput {
147
+ /** Viewport + output width in CSS px. Default 1280. */
148
+ width?: number;
149
+ /** Viewport + output height in CSS px. Default 800. */
150
+ height?: number;
151
+ /** Render scale (2 = retina-crisp capture, downscaled into the video). Default 2. */
152
+ deviceScaleFactor?: number;
153
+ /** Output frames per second (re-encoded from the recording). Default 30. */
154
+ fps?: number;
155
+ /** Time to scroll from top to bottom (ms). Default 6000. */
156
+ duration?: number;
157
+ /** Easing for the default top→bottom scroll. Default "easeInOutCubic". */
158
+ easing?: Easing;
159
+ /** Dwell at the top before scrolling (ms). Default 500. */
160
+ startDelayMs?: number;
161
+ /** Dwell at the bottom after scrolling (ms). Default 800. */
162
+ endDwellMs?: number;
163
+ /** Page-load milestone to wait for before recording. Default "networkidle". */
164
+ waitUntil?: "load" | "domcontentloaded" | "networkidle" | "commit";
165
+ /** Optional element to wait for before recording (e.g. a hero section). Omit to skip. */
166
+ waitForSelector?: string;
167
+ /** x264 quality, 0–51 (lower = better quality / larger file). Default 18. */
168
+ crf?: number;
169
+ /**
170
+ * Capture strategy. "frames" (default) steps a virtual clock per frame — frame-accurate, crisp,
171
+ * reproducible. "realtime" records the live session; use it only for time-based hero animation or
172
+ * autoplay video. Default "frames".
173
+ */
174
+ capture?: "frames" | "realtime";
175
+ /** Parallel render workers for "frames" (each its own browser context). Omit to auto-pick by cores. */
176
+ workers?: number;
177
+ /** Intermediate frame format for "frames". "jpeg" (default) is faster; "png" is lossless. Default "jpeg". */
178
+ frameFormat?: "jpeg" | "png";
179
+ /**
180
+ * Choreographed scroll: an ordered list of steps instead of one top→bottom sweep ("frames" only).
181
+ * Omit for the default single eased sweep.
182
+ */
183
+ choreography?: ChoreographyStepInput[];
184
+ /**
185
+ * Auto-choreograph: detect the page's sections and pan/hold through them ("frames" only). `true`
186
+ * for defaults, or an object to tune. Ignored if `choreography` is set. Omit to disable.
187
+ */
188
+ autoSections?: boolean | AutoSectionsInput;
189
+ /** Loop style. "boomerang" plays the scroll forward then back for a seamless loop. Default "none". */
190
+ loop?: "none" | "boomerang";
191
+ /** Ken Burns slow zoom over the clip ("frames" only). Omit for no zoom. */
192
+ kenBurns?: KenBurnsInput;
193
+ /**
194
+ * Drive a scripted interaction instead of an auto-scroll (move/click/hover/type/scrollTo/wait).
195
+ * Setting this records in REALTIME and emits a single asset (variants/aspect/extra outputs skipped).
196
+ */
197
+ actions?: InteractionActionInput[];
198
+ /** The synthetic cursor shown during an interaction. Omit for the default cursor. */
199
+ cursor?: CursorInput;
200
+ /**
201
+ * Element-focused clip: scroll one component into view, optionally trigger it, hold, and crop the
202
+ * output to its box. Realtime; emits a single asset (variants/aspect/outputs/cards skipped).
203
+ */
204
+ focus?: FocusInput;
205
+ /** Force a color scheme. "both" emits a light AND a dark asset (<name>-light / <name>-dark). Omit to leave as-is. */
206
+ colorScheme?: "light" | "dark" | "both";
207
+ /** Add this class to <html> before capture (e.g. to trigger a CSS-class dark theme). Omit for none. */
208
+ themeClass?: string;
209
+ /** Also capture the reel at these viewports; each emits an asset (<name>-<viewport name>). */
210
+ viewports?: ViewportInput[];
211
+ /**
212
+ * Capture several routes and concatenate them into one reel ("frames" path). Emits a single asset
213
+ * (variants skipped; aspect/outputs apply to the final tour).
214
+ */
215
+ routes?: RouteInput[];
216
+ /** Hide elements matching these CSS selectors before capture (cookie banners, chat widgets, …). Default none. */
217
+ hideSelectors?: string[];
218
+ /** Extra CSS injected before capture (e.g. a brand backdrop, or hiding a sticky header). Omit for none. */
219
+ injectCss?: string;
220
+ /** Click these selectors once after load to dismiss overlays (consent dialogs); best-effort. Default none. */
221
+ clickSelectors?: string[];
222
+ /** Hide scrollbars so they don't appear in the capture. Default true. */
223
+ hideScrollbars?: boolean;
224
+ /** Pause CSS animations/transitions for fully static, deterministic frames. Default false. */
225
+ pauseAnimations?: boolean;
226
+ /** Freeze Date.now / performance.now / Math.random (seeded) so time/random content is stable. Default false. */
227
+ freezeClock?: boolean;
228
+ /** Abort common analytics/ads/session-replay requests during capture (cleaner, faster). Default true. */
229
+ blockTrackers?: boolean;
230
+ /** Extra hostname substrings to block during capture. Default none. */
231
+ blockHosts?: string[];
232
+ /** Playwright resource types to block (e.g. "media", "font", "image"). Default none. */
233
+ blockResourceTypes?: string[];
234
+ /** Wait for fonts + in-view images before each frame's screenshot ("frames"). Defaults on (off in draft). */
235
+ settlePerFrame?: boolean;
236
+ /** Max time (ms) to wait per frame for settling before screenshotting anyway. Default 250. */
237
+ settleMaxMs?: number;
238
+ /** Reframe the output to a target aspect: a preset ("16:9"|"9:16"|"1:1") or explicit {width,height}. Omit to keep the capture aspect. */
239
+ aspect?: AspectInput;
240
+ /** How to fit the capture into `aspect`: "cover" (scale + center-crop) or "contain" (scale + pad). Default "cover". */
241
+ fit?: "cover" | "contain";
242
+ /** Pad color used by "contain". Default "#0b0b0f". */
243
+ padColor?: string;
244
+ /** Files to emit per variant; each becomes its own asset. Default ["mp4"]. */
245
+ outputs?: ("mp4" | "gif" | "webp" | "poster")[];
246
+ /** GIF / animated-WebP frame rate. Defaults to min(fps, 15). */
247
+ gifFps?: number;
248
+ /** Intro card shown before the reel (fades from black). Applies to frames / route tours. Omit for none. */
249
+ intro?: CardInput;
250
+ /** Outro / end card shown after the reel. Omit for none. */
251
+ outro?: CardInput;
252
+ /** Timed on-screen annotations (caption text, a highlight ring, or a spotlight on a selector). */
253
+ annotations?: AnnotationInput[];
254
+ /** Output filename; defaults to "<slug(asset name)>.mp4". */
255
+ fileName?: string;
256
+ }
257
+ /** Author-facing input (documented for editor hover; the schema validates it at run time). */
258
+ type ScrollReelOptions = ScrollReelOptionsInput;
259
+
260
+ /** A named viewport to capture at; each breakpoint emits its own asset. */
261
+ interface BreakpointInput {
262
+ /** Label for this viewport — used in the filename + manifest id (e.g. "desktop"). */
263
+ name: string;
264
+ /** Viewport width in CSS px. */
265
+ width: number;
266
+ /**
267
+ * Viewport height in CSS px. Default 900. Ignored for `fullPage` shots (Playwright resizes to the
268
+ * full page height); only affects viewport/element captures.
269
+ */
270
+ height?: number;
271
+ /** Override the generator-level `deviceScaleFactor` for this breakpoint. Omit to inherit it. */
272
+ deviceScaleFactor?: number;
273
+ }
274
+ /** A specific element to capture (in addition to the page) at each breakpoint. */
275
+ interface ElementShotInput {
276
+ /** CSS selector of the element to shoot. */
277
+ selector: string;
278
+ /** Name used in the filename + manifest id for this element shot. */
279
+ name: string;
280
+ }
281
+ /**
282
+ * Author-facing options for the `screenshots` generator — responsive stills of a page (one per
283
+ * breakpoint), plus optional per-element crops. Everything is optional; sensible defaults apply.
284
+ */
285
+ interface ScreenshotsOptionsInput {
286
+ /**
287
+ * Viewports to capture at (at least one); each emits its own asset. Default:
288
+ * desktop 1440×900 + mobile 390×844.
289
+ */
290
+ breakpoints?: BreakpointInput[];
291
+ /** Capture the entire scrollable page (vs. just the viewport). Default true. */
292
+ fullPage?: boolean;
293
+ /** Image format. Default "png". */
294
+ format?: "png" | "jpeg";
295
+ /** JPEG quality, 1–100 (jpeg only; rejected for png). Omit for the encoder default. */
296
+ quality?: number;
297
+ /** Render scale (2 = retina-crisp). Default 2. */
298
+ deviceScaleFactor?: number;
299
+ /** Page-load milestone to wait for before capturing. Default "networkidle". */
300
+ waitUntil?: "load" | "domcontentloaded" | "networkidle" | "commit";
301
+ /** Optional element to wait for before capturing (e.g. a hero image). Omit to skip. */
302
+ waitForSelector?: string;
303
+ /** Specific elements to crop (in addition to the page) at every breakpoint. Default none. */
304
+ elements?: ElementShotInput[];
305
+ /** Capture with a transparent background (png only). Default false. */
306
+ omitBackground?: boolean;
307
+ /** Extra settle time after load before capturing (ms). Default 0. */
308
+ settleMs?: number;
309
+ }
310
+ /** Author-facing input (documented for editor hover; the schema validates it at run time). */
311
+ type ScreenshotsOptions = ScreenshotsOptionsInput;
312
+
313
+ /** The wall's easing curves (kebab-cased, matching the rest of the tool). */
314
+ declare const wallEasingEnum: z.ZodEnum<["linear", "ease-in", "ease-out", "ease-in-out", "ease-in-out-strong"]>;
315
+ /** The wall's easing curves. */
316
+ type WallEasing = z.infer<typeof wallEasingEnum>;
317
+ /** One eased move — the uniform motion primitive (columns, wall default, and pan all use it). */
318
+ interface WallPulseInput {
319
+ /** When the pulse starts, as a fraction of the clip (0..1). */
320
+ at: number;
321
+ /** How long the move takes, as a fraction of the clip (0..1). If `at + duration > 1`, the start
322
+ * shifts back so the move ends at the loop point. */
323
+ duration: number;
324
+ /** How far it travels, in periods (1 = one full tile-set / one wrap). Usually 0..1. */
325
+ distance: number;
326
+ /** Easing of the move's ramp. Default "ease-in-out". */
327
+ easing?: WallEasing;
328
+ }
329
+ interface WallPanInput {
330
+ /** Pan direction. Default "left". */
331
+ direction?: "left" | "right";
332
+ /** Continuous whole-clip horizontal loops (0 = no pan unless `pulses` move it). Default 0. */
333
+ loops?: number;
334
+ /** Pulses added on top of the base loops. Default none. */
335
+ pulses?: WallPulseInput[];
336
+ }
337
+ /** One column of the wall: its tiles (assets by name) + its own optional Y motion. */
338
+ interface WallColumnInput {
339
+ /** Assets stacked in this column, by name (cycled to fill the height). At least one. */
340
+ tiles: string[];
341
+ /** Constant start-position shift, 0..1 of a tile-set — de-aligns columns with similar content. Default 0. */
342
+ stagger?: number;
343
+ /** Scroll direction. Defaults to "down". */
344
+ direction?: "up" | "down";
345
+ /** Continuous whole-clip loops for this column. Omit to inherit the wall-level `loops`. */
346
+ loops?: number;
347
+ /** This column's pulses. Omit to inherit the wall-level `pulses`. */
348
+ pulses?: WallPulseInput[];
349
+ }
350
+ /** A faux tile for `test` preview mode — a flat color box labeled with its name. */
351
+ interface FauxTileInput {
352
+ /** Box fill (any CSS color). Omit to auto-derive a distinct color from the tile name. */
353
+ color?: string;
354
+ /** Optional caption shown under the name (e.g. "16:9") — purely cosmetic. */
355
+ size?: string;
356
+ /** This faux tile's aspect ratio (width / height): 1.78 = 16:9 (short), 0.56 = 9:16 (tall), 1 =
357
+ * square. Omit to use the wall's `tileAspect` default. Real tiles use their media's own aspect. */
358
+ aspect?: number;
359
+ }
360
+
361
+ /**
362
+ * Author-facing options for the `wall` generator. Each entry in `columns` is a self-contained unit
363
+ * (its `tiles` + its own optional motion); everything else is optional, with defaults noted below.
364
+ * Motion is the uniform pulse model: `loops` (continuous base) + `pulses` (eased moves), summed and
365
+ * rounded up to a whole number of periods so the wall always loops seamlessly.
366
+ */
367
+ interface WallOptionsInput {
368
+ /** Output width in CSS px. Default 1920. */
369
+ width?: number;
370
+ /** Output height in CSS px. Default 1080. */
371
+ height?: number;
372
+ /** Render scale (2 = retina-crisp, downscaled into the video). Default 2. */
373
+ deviceScaleFactor?: number;
374
+ /** Output frames per second. Default 30. */
375
+ fps?: number;
376
+ /** Clip length in seconds — the whole loop. Default 16. */
377
+ durationSeconds?: number;
378
+ /** x264 quality, 0–51 (lower = better quality / larger file). Default 18. */
379
+ crf?: number;
380
+ /**
381
+ * Capture strategy. "frames" (default) is deterministic + parallelizable; "realtime" records the
382
+ * live session. Default "frames".
383
+ */
384
+ capture?: "frames" | "realtime";
385
+ /**
386
+ * Parallel frame-render workers. Video-heavy walls can cold-start to black tiles under many
387
+ * workers — set 1 (or omit) for those. Omit to auto-pick from cores.
388
+ */
389
+ workers?: number;
390
+ /** Intermediate frame format (frames capture). "jpeg" (default) is fast; "png" is lossless. */
391
+ frameFormat?: "jpeg" | "png";
392
+ /** Backdrop shown in the gutters between tiles. Default "#0b0b0f". */
393
+ background?: string;
394
+ /** Output filename; defaults to "<slug(asset name)>.mp4". */
395
+ fileName?: string;
396
+ /**
397
+ * The columns (≥3). Each column lists the assets stacked in it (`tiles`, by name — cycled to fill
398
+ * the height) and may carry its own motion (`direction` / `loops` / `pulses`); omitted `loops` /
399
+ * `pulses` inherit the wall-level defaults below. Column count = `columns.length`.
400
+ */
401
+ columns: WallColumnInput[];
402
+ /** Gap between columns and between tiles (px). Default 8. */
403
+ gap?: number;
404
+ /** Default/fallback tile aspect (width / height). Tiles fit the column width and take their OWN
405
+ * height from their media's aspect (16:9 → short, 9:16 → tall); this is only used for faux
406
+ * (`test`) tiles that don't set their own `aspect`. 0.75 = 3:4 portrait. Default 0.75. */
407
+ tileAspect?: number;
408
+ /** Tile corner radius (px). Default 6. */
409
+ cornerRadius?: number;
410
+ /** The whole wall's horizontal pan (`direction` / `loops` / `pulses`). Default: no pan. */
411
+ pan?: WallPanInput;
412
+ /** Default continuous whole-clip loops for columns that omit their own `loops`. Default 0 (static
413
+ * unless a pulse moves it — a single pulse then rounds up to one loop). */
414
+ loops?: number;
415
+ /** Default pulses for columns that omit their own `pulses` (the uniform wall-level motion). Default none. */
416
+ pulses?: WallPulseInput[];
417
+ /**
418
+ * Preview mode: render every tile as a flat labeled color box instead of the real assets. No
419
+ * producer assets run, so the wall renders in seconds — flip it on to dial in layout + motion,
420
+ * then off for the real render. Default false.
421
+ */
422
+ test?: boolean;
423
+ /** Per-tile faux appearance for `test` mode, keyed by tile name (color + caption). Default {} (auto colors + names). */
424
+ testTiles?: Record<string, FauxTileInput>;
425
+ }
426
+ /** Author-facing input (documented for editor hover; the schema validates it at run time). */
427
+ type WallOptions = WallOptionsInput;
428
+
429
+ /** One "pulse" (beat) of the animation storyboard. */
430
+ interface PulseInput {
431
+ /** Human label for the beat, e.g. "color sweep" — purely to keep the config readable. */
432
+ name?: string;
433
+ /** Length of this beat, in seconds. */
434
+ duration: number;
435
+ /** Fraction of cells whose glyph changes during this beat (0..1; 1 = every cell once; 0 = a hold). */
436
+ chars?: number;
437
+ /** Fraction of cells whose color changes during this beat (0..1; 1 = every cell once). */
438
+ colors?: number;
439
+ /**
440
+ * Target color for this beat's color changes. When set, every color change goes to this exact
441
+ * token (a deliberate sweep) rather than a weighted-random pick. Set `colors: 1` with
442
+ * `pacing: "even"` to evenly wash the whole specimen to one color. Omit for the default
443
+ * scattered, weighted-random recoloring.
444
+ */
445
+ color?: "foreground" | "muted" | "accent";
446
+ /**
447
+ * How the changes are distributed in time across the beat — like a CSS easing curve:
448
+ * "linear"/"even" = uniform, "ease-in" = front-loaded, "ease-out" = back-loaded,
449
+ * "ease-in-out" = bunched at both ends, "random" = scattered.
450
+ */
451
+ pacing?: "even" | "linear" | "ease-in" | "ease-out" | "ease-in-out" | "random";
452
+ }
453
+ /**
454
+ * Relative likelihood each color token is chosen on a (non-targeted) color change. Higher = more
455
+ * frequent. The default keeps foreground/muted common and accent a rare pop (2 / 2 / 1). Set any to
456
+ * 0 to exclude that token from random recoloring (an explicit pulse `color` can still target it).
457
+ */
458
+ interface SpecimenColorWeightsInput {
459
+ foreground?: number;
460
+ muted?: number;
461
+ accent?: number;
462
+ }
463
+ /** The specimen's color palette (any CSS colors). Override any subset. */
464
+ interface SpecimenColorsInput {
465
+ /** Backdrop behind the glyphs. */
466
+ background?: string;
467
+ /** Primary glyph color — the resting majority. */
468
+ foreground?: string;
469
+ /** Muted/secondary glyph color. */
470
+ muted?: string;
471
+ /** Accent color for occasional pops; defaults to `background` (accent glyphs blend in) if unset. */
472
+ accent?: string;
473
+ /** Color of the font-name label (bottom corner); defaults to `foreground` if unset. */
474
+ label?: string;
475
+ }
476
+ /** A named option preset. The keys here are the selectable `template` values. */
477
+ type SpecimenTemplate = "demo" | "sweep";
478
+ /**
479
+ * Author-facing options for the `specimen` generator — a looping type-specimen video. Only `font`
480
+ * is required; everything else has a sensible default.
481
+ */
482
+ interface SpecimenOptionsInput {
483
+ /** Font file to showcase (path relative to the working dir, or absolute). Required. */
484
+ font: string;
485
+ /**
486
+ * Load a named option preset; your explicit options below still override what it sets. Options:
487
+ * - `"demo"` — labeled walkthrough of every pulse behavior (each easing curve, a color sweep, a
488
+ * mingle) with demo mode on; runs once, no mirror. Good for seeing what each setting does.
489
+ * - `"sweep"` — seamless-looping showcase of the even per-character color sweeps (muted → accent
490
+ * → foreground) on a dark palette chosen so the accent reads.
491
+ */
492
+ template?: SpecimenTemplate;
493
+ /** Display name shown bottom-left (e.g. "ABC Oracle"). Default none. */
494
+ name?: string;
495
+ /** Demo mode: overlay the active pulse's name bottom-right, to see which beat is playing. Default false. */
496
+ demo?: boolean;
497
+ /** Output frames per second. Default 30. */
498
+ fps?: number;
499
+ /** Clip length in seconds. Defaults to the (mirrored) sum of the pulse durations; set to override. */
500
+ durationSeconds?: number;
501
+ /** Output frame width in px. Default 1920. */
502
+ width?: number;
503
+ /** Output frame height in px. Default 1080. */
504
+ height?: number;
505
+ /** Render scale (1 = 1:1; higher = crisper capture, downscaled into the video). Default 1. */
506
+ deviceScaleFactor?: number;
507
+ /** Glyph weight on the variable-font axis, 1–1000. Default 820. */
508
+ weight?: number;
509
+ /** Number of glyph rows. The glyph size is derived so the rows fill the top 80% of the frame. Default 3. */
510
+ lines?: number;
511
+ /** Line-height of the glyph block. Default 0.78 (tight, cap-height-hugging). */
512
+ leading?: number;
513
+ /** Glyphs to exclude from the showcase, e.g. "QXZ" (case-insensitive). Default none. */
514
+ blacklist?: string;
515
+ /** Override the glyph pool the specimen draws from (≥2 distinct characters). Default A–Z 0–9 + symbols. */
516
+ characterPool?: string;
517
+ /** Schedule seed — same seed ⇒ identical animation. Change for a different (still deterministic) take. Default 1. */
518
+ seed?: number;
519
+ /** Color tokens the glyphs cycle through. Override any subset. Default: light-grey palette. */
520
+ colors?: SpecimenColorsInput;
521
+ /** Relative likelihood of each color token on a random (non-targeted) color change. Default 2 / 2 / 1. */
522
+ colorWeights?: SpecimenColorWeightsInput;
523
+ /** The animation storyboard: an ordered sequence of pulses (beats). Default: a lively built-in storyboard. */
524
+ pulses?: PulseInput[];
525
+ /** Multiply every pulse's glyph-change fraction (1 = baseline, 2 = twice as busy, 0 = none). Default 1. */
526
+ characterIntensity?: number;
527
+ /** Multiply every pulse's color-change fraction (1 = baseline, 2 = twice as busy, 0 = none). Default 1. */
528
+ colorIntensity?: number;
529
+ /**
530
+ * Max fraction a line's total width may drift as its glyphs change. Default 0.05. Glyph swaps are
531
+ * width-compensated to stay within this, so the left-aligned right edge barely moves.
532
+ */
533
+ maxLineDrift?: number;
534
+ /**
535
+ * Mirror the pulses (play them out and back) so the clip ends on its opening frame and loops
536
+ * seamlessly. Doubles the clip length. Set false for a one-shot that ends on the last state. Default true.
537
+ */
538
+ mirror?: boolean;
539
+ /** x264 quality, 0–51 (lower = better quality / larger file). Default 18. */
540
+ crf?: number;
541
+ /** Output filename; defaults to "<slug(asset name)>.mp4". */
542
+ fileName?: string;
543
+ }
544
+ /** Author-facing input (documented for editor hover; the schema above validates it at run time). */
545
+ type SpecimenOptions = SpecimenOptionsInput;
546
+
547
+ /** A field that can be shown on a swatch. */
548
+ type FieldId = "name" | "hex" | "rgb" | "oklch" | "hsl" | "cmyk";
549
+
550
+ /** One color in the palette. */
551
+ interface PaletteColorInput {
552
+ /** Display name, e.g. "Wet Grey". */
553
+ name: string;
554
+ /** Hex value (#rgb or #rrggbb, with or without #). */
555
+ hex: string;
556
+ }
557
+ /**
558
+ * Author-facing options for the `palette` generator — a still color-palette image. Each color is a
559
+ * swatch labeled with the fields you place in its corners (name / hex / rgb / oklch / hsl / cmyk),
560
+ * with auto-contrasting text. Only `colors` is required.
561
+ */
562
+ interface PaletteOptionsInput {
563
+ /** The colors to show (at least one). */
564
+ colors: PaletteColorInput[];
565
+ /** Swatch arrangement: full-width bands, full-height columns, or an N-wide grid. Default "rows". */
566
+ layout?: "rows" | "columns" | "grid";
567
+ /** Columns when `layout: "grid"`. Default 3. */
568
+ gridColumns?: number;
569
+ /** Output width in px. Default 1400. */
570
+ width?: number;
571
+ /** Output height in px. Default 1750 (portrait 4:5 with the default width). */
572
+ height?: number;
573
+ /** Render scale (2 = retina-crisp). Default 2. */
574
+ deviceScaleFactor?: number;
575
+ /** Page background, shown only in the gaps between swatches. Default "#ffffff". */
576
+ background?: string;
577
+ /** Gap between swatches (px). Default 0 (swatches abut). */
578
+ gap?: number;
579
+ /** Swatch corner radius (px). Default 0 (square). */
580
+ cornerRadius?: number;
581
+ /** Fields stacked in the top-left corner. Default name + hex. */
582
+ topLeft?: FieldId[];
583
+ /** Fields stacked in the top-right corner. Default rgb + oklch. */
584
+ topRight?: FieldId[];
585
+ /** Fields stacked in the bottom-left corner. Default none. */
586
+ bottomLeft?: FieldId[];
587
+ /** Fields stacked in the bottom-right corner. Default none. */
588
+ bottomRight?: FieldId[];
589
+ /** Uppercase the color names. Default false. */
590
+ uppercase?: boolean;
591
+ /** RGB string style. Default "labeled". */
592
+ rgbStyle?: "labeled" | "css" | "plain";
593
+ /** OKLCH string style. Default "css". */
594
+ oklchStyle?: "css" | "labeled";
595
+ /** Custom font file (woff2/woff/ttf/otf), embedded into the render. Omit for a system bold sans. */
596
+ fontFile?: string;
597
+ /** Label font size in px. Omit to derive from the width. */
598
+ fontSize?: number;
599
+ /** Label font weight. Default 700. */
600
+ fontWeight?: number;
601
+ /** Light text color, used on dark swatches (picked by contrast). Default "#ffffff". */
602
+ textLight?: string;
603
+ /** Dark text color, used on light swatches (picked by contrast). Default "#141414". */
604
+ textDark?: string;
605
+ /** Luminance above which the dark text is used (0..1). Default 0.5. */
606
+ contrastThreshold?: number;
607
+ /** Inset of the labels from the swatch edges (px). Omit to derive from the width. */
608
+ padding?: number;
609
+ /** Output filename; defaults to "<slug(asset name)>.png". */
610
+ fileName?: string;
611
+ }
612
+ /** Author-facing input (documented for editor hover; the schema validates it at run time). */
613
+ type PaletteOptions = PaletteOptionsInput;
614
+
615
+ /**
616
+ * Author-facing options for the `palette-reel` generator — a looping reveal *video* of a color
617
+ * palette (the moving counterpart of the still `palette` generator). The colors start as thin
618
+ * slivers showing only their name; one at a time a sliver expands into a band that reveals its
619
+ * configured `details` (hex / oklch / rgb …), holds, then collapses before the next opens — sweeping
620
+ * every color and looping seamlessly. Only `colors` is required; everything else has a default.
621
+ */
622
+ interface PaletteReelOptionsInput {
623
+ /** The colors to reveal (at least one). */
624
+ colors: PaletteColorInput[];
625
+ /** Sliver arrangement: horizontal bands (names upright) or full-height vertical strips. Default "rows". */
626
+ orientation?: "rows" | "columns";
627
+ /** Fields revealed when a color expands (the name is always shown, so it's ignored here). Default hex + oklch + rgb. */
628
+ details?: FieldId[];
629
+ /** How long each color stays fully open before handing off to the next. Default 2. */
630
+ holdSeconds?: number;
631
+ /** Crossfade length from one open color to the next. Default 0.7. */
632
+ transitionSeconds?: number;
633
+ /**
634
+ * Ping-pong the sweep (down the list then back up) so every handoff is between neighbouring bands —
635
+ * the open band only ever slides by one, avoiding the "pinch" of a last→first jump at the loop seam.
636
+ * Off wraps directly (last→first): shorter, but crossfades non-adjacent bands at the seam. Default true.
637
+ */
638
+ bounce?: boolean;
639
+ /** Easing applied to the crossfade ramp. Default "ease-in-out". */
640
+ easing?: "linear" | "ease-in" | "ease-out" | "ease-in-out";
641
+ /** Clip length override (s). Omit to derive (count × (hold + transition)) for a clean loop. */
642
+ durationSeconds?: number;
643
+ /** How many times a sliver's share a fully-open band takes (a collapsed sliver is the baseline). Default 12. */
644
+ grownFlex?: number;
645
+ /** Minimum cross-size of a sliver in px so its name stays legible. Default 0 (derive from height). */
646
+ minCrossPx?: number;
647
+ /** Keep the name fully visible even in a collapsed sliver (else it fades with the band). Default true. */
648
+ nameAlwaysVisible?: boolean;
649
+ /** Uppercase the color names. Default false. */
650
+ uppercase?: boolean;
651
+ /** RGB string style. Default "labeled". */
652
+ rgbStyle?: "labeled" | "css" | "plain";
653
+ /** OKLCH string style. Default "css". */
654
+ oklchStyle?: "css" | "labeled";
655
+ /** Light text color, used on dark bands (picked by contrast). Default "#ffffff". */
656
+ textLight?: string;
657
+ /** Dark text color, used on light bands (picked by contrast). Default "#141414". */
658
+ textDark?: string;
659
+ /** Luminance above which the dark text is used (0..1). Default 0.5. */
660
+ contrastThreshold?: number;
661
+ /** Custom font file (woff2/woff/ttf/otf), served into the render. Omit for a system bold sans. */
662
+ fontFile?: string;
663
+ /** Label font weight. Default 700. */
664
+ fontWeight?: number;
665
+ /** Name font size in px. Omit to derive from the frame size. */
666
+ fontSize?: number;
667
+ /** Detail-line font size as a fraction of the name size. Default 0.62. */
668
+ detailFontScale?: number;
669
+ /** Backdrop behind the bands (shown in `gap` between them). Default "#ffffff". */
670
+ background?: string;
671
+ /** Gap between bands (px). Default 0 (bands abut). */
672
+ gap?: number;
673
+ /** Band corner radius (px). Default 0 (square). */
674
+ cornerRadius?: number;
675
+ /** Output frame width in px. Default 1920. */
676
+ width?: number;
677
+ /** Output frame height in px. Default 1080. */
678
+ height?: number;
679
+ /** Render scale (higher = crisper capture, downscaled into the video). Default 1. */
680
+ deviceScaleFactor?: number;
681
+ /** Output frames per second. Default 30. */
682
+ fps?: number;
683
+ /** x264 quality, 0–51 (lower = better quality / larger file). Default 18. */
684
+ crf?: number;
685
+ /** Output filename; defaults to "<slug(asset name)>.mp4". */
686
+ fileName?: string;
687
+ }
688
+ /** Author-facing input (documented for editor hover; the schema validates it at run time). */
689
+ type PaletteReelOptions = PaletteReelOptionsInput;
690
+
691
+ /**
692
+ * Author-facing options for the `image` generator — a passthrough that records an existing image
693
+ * file as an asset so it can feed a scene (e.g. high-resolution photos as media-wall tiles) or be
694
+ * tracked in the manifest. Only `src` is required.
695
+ */
696
+ interface ImageOptions {
697
+ /** Path to the source image (relative to the cwd, or absolute). */
698
+ src: string;
699
+ /** Output filename; defaults to "<slug(asset name)><ext of src>". */
700
+ fileName?: string;
701
+ }
702
+
703
+ /**
704
+ * Author-facing config types. These power editor autocomplete in `pro-visu.config.ts`.
705
+ * The runtime validator lives in `schema.ts`; each generator validates its own options.
706
+ * When a new generator is added, extend `AssetSpecInput` and `defaults` here.
707
+ */
708
+ type LogLevel = "silent" | "error" | "warn" | "info" | "debug";
709
+ interface BrowserSettingsInput {
710
+ /** Run the browser without a visible window (default true). Set false to watch captures. */
711
+ headless?: boolean;
712
+ /** Browser channel, e.g. "chrome" or "msedge". Omit to use the managed Chromium. */
713
+ channel?: string;
714
+ /** Absolute path to a browser executable (overrides `channel` + managed Chromium). */
715
+ executablePath?: string;
716
+ /** Extra launch args, e.g. ["--no-sandbox"] on CI. */
717
+ args?: string[];
718
+ /** Browser launch timeout (ms). */
719
+ timeout?: number;
720
+ }
721
+ interface ServerSettingsInput {
722
+ /**
723
+ * Command that starts the server, run via the shell. The tool sets PORT/HOST in its environment
724
+ * to the readiness port/host, so frameworks that honor PORT (Next, Vite, …) bind it
725
+ * automatically — `command: "next start"` is enough. An explicit flag still wins.
726
+ */
727
+ command: string;
728
+ /** Optional one-shot build to run first, e.g. "next build". */
729
+ build?: string;
730
+ /** Health-check URL polled until it responds. Defaults to http://127.0.0.1:<port>. */
731
+ url?: string;
732
+ /**
733
+ * Port the readiness check polls — also derives `url` when `url` is omitted, and is passed to
734
+ * the command as PORT so it binds the same port automatically. Defaults to 3101.
735
+ */
736
+ port?: number;
737
+ /** Working dir for build + command, relative to the config dir. Defaults to it. */
738
+ cwd?: string;
739
+ /** Max time to wait for the server to become reachable (ms). Default 120000. */
740
+ readyTimeoutMs?: number;
741
+ /** If a server is already reachable at the URL, use it as-is (don't start/stop one). */
742
+ reuseExisting?: boolean;
743
+ }
744
+ interface ShowcaseSettingsInput {
745
+ /** Output directory for generated assets, relative to the repo root (default "pro-visu"). */
746
+ outDir?: string;
747
+ /** How many assets to generate in parallel (shared browser, separate contexts). */
748
+ concurrency?: number;
749
+ /**
750
+ * Raise the Node heap (V8 old-space) to this many MB. Heavy jobs — large frame-stepped walls
751
+ * especially — can exceed Node's ~4 GB default and crash with "JavaScript heap out of memory".
752
+ * When set above the current limit, the CLI re-execs itself with `--max-old-space-size`. This is
753
+ * the Node process heap, not the browser's.
754
+ */
755
+ maxMemoryMB?: number;
756
+ /** CLI log verbosity. */
757
+ logLevel?: LogLevel;
758
+ /** Playwright launch controls. */
759
+ browser?: BrowserSettingsInput;
760
+ /** Build → start → wait → capture → stop a server automatically. */
761
+ server?: ServerSettingsInput;
762
+ /** "draft" lowers fps/scale and speeds the encoder for fast iteration. */
763
+ quality?: "draft" | "final";
764
+ /** Skip assets whose inputs+options+tool fingerprint is unchanged (opt-in). */
765
+ cache?: boolean;
766
+ /** Per-generator option defaults, keyed by generator id, merged under each asset. */
767
+ defaults?: {
768
+ "scroll-reel"?: ScrollReelOptions;
769
+ screenshots?: ScreenshotsOptions;
770
+ specimen?: SpecimenOptions;
771
+ palette?: PaletteOptions;
772
+ "palette-reel"?: PaletteReelOptions;
773
+ };
774
+ }
775
+ /** Fields common to every asset. */
776
+ interface AssetBaseInput {
777
+ /** Unique id for this asset — also the output filename (`<slug(name)>.mp4`) and manifest key. */
778
+ name: string;
779
+ /** Other assets this one consumes, as `{ slotName: assetName }`. Producers run first. */
780
+ inputs?: Record<string, string>;
781
+ }
782
+ /**
783
+ * Discriminated by `generator` so each asset gets the right `options` autocomplete. URL-based
784
+ * generators take a `url` — absolute, or a `/path` resolved against the managed server; omit it
785
+ * to capture the managed server's root. A local `scene` composites its `inputs` and needs none.
786
+ */
787
+ type AssetSpecInput = (AssetBaseInput & {
788
+ url?: string;
789
+ generator: "scroll-reel";
790
+ options?: ScrollReelOptions;
791
+ }) | (AssetBaseInput & {
792
+ url?: string;
793
+ generator: "screenshots";
794
+ options?: ScreenshotsOptions;
795
+ }) | (AssetBaseInput & {
796
+ generator: "wall";
797
+ options?: WallOptions;
798
+ }) | (AssetBaseInput & {
799
+ generator: "specimen";
800
+ options: SpecimenOptions;
801
+ }) | (AssetBaseInput & {
802
+ generator: "palette";
803
+ options: PaletteOptions;
804
+ }) | (AssetBaseInput & {
805
+ generator: "palette-reel";
806
+ options: PaletteReelOptions;
807
+ }) | (AssetBaseInput & {
808
+ generator: "image";
809
+ options: ImageOptions;
810
+ });
811
+ interface ShowcaseUserConfig {
812
+ settings?: ShowcaseSettingsInput;
813
+ assets: AssetSpecInput[];
814
+ }
815
+ /** Identity helper that gives `pro-visu.config.ts` full type-checking + autocomplete. */
816
+ declare function defineConfig(config: ShowcaseUserConfig): ShowcaseUserConfig;
817
+
818
+ export { type AssetSpecInput, type BrowserSettingsInput, type LogLevel, type ScreenshotsOptions, type ScrollReelOptions, type ServerSettingsInput, type ShowcaseSettingsInput, type ShowcaseUserConfig, type SpecimenOptions, type WallOptions, defineConfig };