@kitlangton/knobs 0.4.0-next.1 → 0.4.0-next.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 CHANGED
@@ -1328,6 +1328,68 @@ Click or `Enter` zooms a tile to fill the view, aspect-fit and centred: one cont
1328
1328
 
1329
1329
  The handle has `update(items)`, `zoom(id | null)`, `zoomed`, `active`, `focus(id?)`, `picks`, `pick(ids)`, `plan`, `moving`, `stats()` (the frame loop's script time) and `dispose()`; `onZoom` and `onActive` report changes. It takes the pieces' tokens and `restyle`, in dark and light; tiles round at `--k-ui-bento-radius` (3px), square under `--k-frame: flat`; `--k-ui-bento-bg` and `--k-ui-scrim` set the page under the tiles and the zoom. `planBento(aspects, { width, height, … })` is the layout on its own, with `bentoNeighbour`, `bentoWindow` and `fitRect`. `Bento` in `knobs/ui/react` and `knobs/ui/solid` follows `items` and `picks`. The demo is `examples/ideas.html` (`?n=500` for a big set, `?groups`, `?hero=3`, `?perf` for a frame-time readout, `?bench` to scroll and zoom on its own and report).
1330
1330
 
1331
+ ### Walkthrough
1332
+
1333
+ ```ts
1334
+ import { walkthrough, type WalkthroughDoc } from "knobs/ui"
1335
+
1336
+ const doc: WalkthroughDoc = {
1337
+ id: "2026-10-19-onboarding-prs",
1338
+ title: "Onboarding audit: before / after",
1339
+ sections: [
1340
+ {
1341
+ title: "Refresh models globally",
1342
+ link: "https://github.com/anomalyco/opencode/pull/57148",
1343
+ status: "ready",
1344
+ steps: [{ title: "Desktop model picker", media: { kind: "compare", before: "a.mp4", after: "b.mp4" } }],
1345
+ },
1346
+ ],
1347
+ }
1348
+ const w = walkthrough(container, { doc, onFeedback: (fb) => send(fb) })
1349
+ ```
1350
+
1351
+ **`walkthrough(container, { doc })`** is the page an agent writes to show its work: a plain document (JSON) of sections and steps, each step one card holding a caption, its media and what it asks (a note, or a pick between options). Notes come from a bar docked at the bottom, typed, spoken or inked, each carrying what was on show (a video's time and chapter, a compare's loop and zoom, a diff's file) and a short log of what led up to it; `onFeedback`, `w.feedback` and `w.markdown()` hand it back. `j` / `k` move between steps, `←` / `→` seek, `f` goes full window, `n` writes a note, `1`–`9` pick. Media is a bare URL (a video or picture), or `compare`, `video`, `image`, `diff`, `code`, `terminal`, `metric`, `callout`, `text`, `history`, `stack`, `table`, or `mount` (your own, `(el) => cleanup`; the one that isn't JSON). A step's card is the surface, so media inside draw no card of their own. Wherever media name a step (`step` on a history event, a stack item or a table cell) they take its key, id or number (`"1.2"`), and clicking goes there as `j` / `k` do. The demo is `examples/walkthrough.html`.
1352
+
1353
+ **`history`**: dated events down a spine, a tone dot each (`good`, `warn`, `bad`, `info`, `dim`), under quiet group headings. `provenance` says whether an event was seen in the record (`"verified"`) or worked out from behaviour (`"inferred"`, a ring for a dot and a dashed tag), and `source` where (`"verified · git"`). `evidence` is small tags, linked or not; `type: "release"` draws a diamond, `"audit"` a square. Groups take `events` or `rows`; a bare `events` list is one group. In a narrow card the date goes over the event.
1354
+
1355
+ ```json
1356
+ { "kind": "history", "groups": [{ "heading": "v2 dropped it", "summary": "Jun 26 – Jul 30", "events": [
1357
+ { "date": "Jun 26, 2026", "age": "116d ago", "title": "The v2 TUI rewrite leaves out DialogRetryAction",
1358
+ "detail": "`session/index.tsx` replaced without the listener.", "tone": "bad",
1359
+ "provenance": "verified", "source": "git", "evidence": [{ "label": "e6f660fecf" }] },
1360
+ { "date": "Oct 20, 2026", "title": "/install still 307s to v1", "tone": "warn",
1361
+ "provenance": "inferred", "source": "edge", "step": "4.8" } ] }] }
1362
+ ```
1363
+
1364
+ **`stack`**: PRs in merge order as a branch graph. `branch` is `"head → base"` (or `head` and `base`); an item whose base is another's head hangs under it, joined by a drawn rail, and the rest sit on the trunk (`base`, else the branch most items merge into), named at the top. An item on a branch that's neither shows it. `delta` is `"+164 / -18"` or `{ add, del }`, drawn green and red; `status` and `tone` make its chip; `step` (or `stepId`) makes its title go to that step; `href` links its PR number.
1365
+
1366
+ ```json
1367
+ { "kind": "stack", "base": "dev", "items": [
1368
+ { "order": "01", "pr": "#57146", "href": "https://github.com/anomalyco/opencode/pull/57146", "title": "Typed free-limit error",
1369
+ "branch": "free-limit-card → dev", "delta": "+193 / -8", "status": "Stack base", "tone": "good", "step": "4.1" },
1370
+ { "pr": "#57466", "title": "Restore the Desktop dialog", "branch": "free-limit-desktop → free-limit-card",
1371
+ "delta": { "add": 446, "del": 47 }, "status": "Stacked on #57146", "tone": "good", "summary": "Modal, notice and prompt recall." } ] }
1372
+ ```
1373
+
1374
+ **`table`**: a real `<table>` for audits and matrices. Rows are cells by column key, bare or under `cells`; a cell is text, a number, or `{ text, href?, code?, badge?, tone?, step? }` (a `badge` is a tone chip). Columns take `mono` and `align` (number columns go right by themselves). Figures are tabular; a tall table keeps its head stuck, a wide one scrolls across, its edges fade where there's more, and its bars show thin on hover. `caption` goes under it.
1375
+
1376
+ ```json
1377
+ { "kind": "table", "caption": "From a clean HOME, Oct 20", "columns": [
1378
+ { "key": "channel", "label": "Entry point", "mono": true }, { "key": "days", "label": "Days on v2" },
1379
+ { "key": "state", "label": "State", "align": "right" } ],
1380
+ "rows": [ { "cells": { "channel": { "text": "npm i -g @opencode-ai/cli", "code": true }, "days": 39,
1381
+ "state": { "text": "Broken", "badge": "bad" } } },
1382
+ { "channel": "brew install …/opencode-v2", "days": 31, "state": { "text": "Pending", "badge": "info", "step": "4.6" } } ] }
1383
+ ```
1384
+
1385
+ **`chapters`** on a `video` (or a `compare`): timestamp pills under it. A click seeks there and plays, and goes in the note's log; one highlight springs to the chapter playing as it plays (.4 s, no bounce), and a note's `media` names it (`chapter`). A chapter ends at `end`, else `start + duration`, else the next one's start. On a compare they're also its timeline's markers. A video's `captions` (or `captionsSrc`) is a WebVTT URL, shown by default.
1386
+
1387
+ ```json
1388
+ { "kind": "video", "src": "journey.mp4", "poster": "journey.png", "captions": "captions.vtt", "chapters": [
1389
+ { "id": "scorecard", "label": "Scorecard", "start": 0, "duration": 9.57 },
1390
+ { "label": "Today (broken)", "start": 9.57 }, { "label": "After", "start": 35.38 } ] }
1391
+ ```
1392
+
1331
1393
  ## Theming, Density & WebGPU Renderer
1332
1394
 
1333
1395
  Panels take a `theme`, an `appearance`, `density: "compact" | "default" | "comfortable"` and `renderer: "gpu" | "dom" | "auto"` (or `?gpu` in the URL), on `defineControls`, `mountPanel` or `<KnobsPanel />`; every `knobs/ui` piece takes the same `theme` and `appearance`, and `restyle(target, look)` changes them on what's already on screen. The header title defaults to the controls id, written as words (`"synth-voice"` reads "Synth voice"); pass `title` to override it or `title: false` to hide it.
@@ -1818,6 +1880,7 @@ The package has two core exports selected through the standard `development` con
1818
1880
  | `optionPicker(container, { options, selected?, chosen?, onSelect?, onChoose?, body?, digits? })` | Options 1–n on a gliding card, the selected one's parameters (a panel) open under it. |
1819
1881
  | `bento(container, { items, gap?, tolerance?, rowHeight?, fit?, blur?, picks?, onPick?, onZoom?, onActive? })` | An ideas grid: a bento of images and live content that zooms an item to fill the view; picks with Space. |
1820
1882
  | `planBento(aspects, { width, height, gap?, tolerance?, rowHeight?, groups?, hero? })` | The bento's layout on its own: rects, mode, coverage and crop. |
1883
+ | `walkthrough(container, { doc, onFeedback?, storage?, keys?, rail?, advance? })` / `mediaSlot(container, { media, steps? })` | The review page an agent writes (sections, steps, media from compares to histories, stacks and tables, notes and picks handed back); one step's media on its own. |
1821
1884
  | `overlayLook({ preset?, theme? })` | The look every overlay takes. |
1822
1885
  | `hints.register(root, () => targets, { listen?, rank? })` / `data-hint` | Joins the page's hint mode (`s`); `hints.start()`, `hints.stop()`. |
1823
1886
  | `restyle(target, { preset?, theme?, density? })` | Re-themes the pieces and panels already on screen, live; on a document, later overlays too. |
@@ -28,5 +28,5 @@ export { diffActivity, flattenActivity, formatDuration, indexActivity, keepSelec
28
28
  export { optionPicker, type OptionPicker, type OptionPickerOptions, type PickerOption, } from "./picker.js";
29
29
  export { claimsDigit, digitPick, keepPick, stepPick } from "./picker-model.js";
30
30
  export { tasteTest, walkthrough, type TasteTest, type TasteTestOptions, type Walkthrough, type WalkthroughOptions, } from "./walkthrough.js";
31
- export { mediaSlot, type MediaSlot, type MediaSlotOptions } from "./walkthrough-media.js";
32
- export { collectFeedback, feedbackMarkdown, inlineMarkdown, normalizeWalkthrough, readState as readWalkthroughState, walkthroughProgress, type ContextEvent, type FeedbackItem, type FeedbackNote, type FeedItem, type MediaContext, type NoteContext, type WalkInk, type WalkNote, type WalkVoice, type WalkthroughQA, type WalkthroughUpdate, type WalkPage, type WalkStep, type WalkthroughDoc, type WalkthroughFeedback, type WalkthroughMedia, type WalkthroughMeta, type WalkthroughOption, type WalkthroughSection, type WalkthroughSource, type WalkthroughState, type WalkthroughStatus, type WalkthroughStep, } from "./walkthrough-model.js";
31
+ export { mediaSlot, type MediaSlot, type MediaSlotOptions, type StepLinks, } from "./walkthrough-media.js";
32
+ export { collectFeedback, feedbackMarkdown, inlineMarkdown, normalizeWalkthrough, readState as readWalkthroughState, stackLayout, walkthroughProgress, type ContextEvent, type FeedbackItem, type FeedbackNote, type FeedItem, type MediaContext, type NoteContext, type StackLayout, type StackRow, type WalkInk, type WalkNote, type WalkVoice, type WalkthroughQA, type WalkthroughUpdate, type WalkPage, type WalkStep, type WalkthroughChapter, type WalkthroughDoc, type WalkthroughEvidence, type WalkthroughFeedback, type WalkthroughHistoryEvent, type WalkthroughHistoryGroup, type WalkthroughMedia, type WalkthroughMeta, type WalkthroughOption, type WalkthroughSection, type WalkthroughSource, type WalkthroughStackItem, type WalkthroughState, type WalkthroughStatus, type WalkthroughStep, type WalkthroughTableCell, type WalkthroughTableColumn, type WalkthroughTableRow, type WalkthroughTone, } from "./walkthrough-model.js";
@@ -0,0 +1,43 @@
1
+ import { type ContextEvent, type WalkChapter, type WalkMedia } from "./walkthrough-model.js";
2
+ /** How media reach the page's steps: `step` on a history event, a stack item or a table cell. */
3
+ export interface StepLinks {
4
+ /** The step a ref names (its key, id or number), or null when there's none. */
5
+ find(ref: string): {
6
+ readonly key: string;
7
+ readonly number: string;
8
+ readonly title: string;
9
+ } | null;
10
+ /** Goes to the step, as j / k do. */
11
+ go(key: string): void;
12
+ }
13
+ type OnEvent = ((event: Omit<ContextEvent, "at" | "step">) => void) | undefined;
14
+ export interface Block {
15
+ dispose(): void;
16
+ context?(): {
17
+ kind: string;
18
+ };
19
+ }
20
+ export declare function mountHistory(host: HTMLElement, m: Extract<WalkMedia, {
21
+ kind: "history";
22
+ }>, steps: StepLinks | undefined): Block;
23
+ export declare function mountStack(host: HTMLElement, m: Extract<WalkMedia, {
24
+ kind: "stack";
25
+ }>, steps: StepLinks | undefined): Block;
26
+ export declare function mountTable(host: HTMLElement, m: Extract<WalkMedia, {
27
+ kind: "table";
28
+ }>, steps: StepLinks | undefined): Block;
29
+ export interface ChapterPills {
30
+ /** The chapter the time is in (call as it plays or seeks): the highlight springs to it. */
31
+ sync(): void;
32
+ /** The chapter playing, or null. */
33
+ readonly current: WalkChapter | null;
34
+ dispose(): void;
35
+ }
36
+ export declare function chapterPills(host: HTMLElement, chapters: readonly WalkChapter[], io: {
37
+ /** Seconds. */
38
+ now(): number;
39
+ /** Seeks and plays. */
40
+ seek(time: number): void;
41
+ onEvent: OnEvent;
42
+ }): ChapterPills;
43
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { type UiLook } from "./look.js";
2
+ import { type StepLinks } from "./walkthrough-blocks.js";
2
3
  import { type ContextEvent, type MediaContext, type WalkthroughMedia } from "./walkthrough-model.js";
3
4
  export interface MediaSlotOptions extends UiLook {
4
5
  readonly media: WalkthroughMedia;
@@ -6,7 +7,10 @@ export interface MediaSlotOptions extends UiLook {
6
7
  readonly lazy?: boolean;
7
8
  /** Kit played, paused or sought a video, or showed another diff file (for a note's log). */
8
9
  readonly onEvent?: (event: Omit<ContextEvent, "at" | "step">) => void;
10
+ /** The page's steps, for `step` on a history event, a stack item or a table cell. */
11
+ readonly steps?: StepLinks;
9
12
  }
13
+ export type { StepLinks };
10
14
  export interface MediaSlot {
11
15
  readonly element: HTMLElement;
12
16
  /** Whether its content is mounted (heavy kinds wait until they're near the view). */
@@ -7,6 +7,98 @@ export interface WalkthroughSource {
7
7
  readonly src: string;
8
8
  readonly label?: string;
9
9
  }
10
+ /** A status colour: a dot, a chip or a cell's badge. */
11
+ export type WalkthroughTone = "good" | "warn" | "bad" | "info" | "dim";
12
+ /** A small tag that backs a claim: a PR, a commit, a log, as a link or not. */
13
+ export interface WalkthroughEvidence {
14
+ readonly label: string;
15
+ readonly href?: string;
16
+ }
17
+ /** One dated moment in a `history`. */
18
+ export interface WalkthroughHistoryEvent {
19
+ /** As written: "Apr 9, 2026", "2026-04-09", "May 7–8". */
20
+ readonly date: string;
21
+ /** How long ago, or how long it lasted: "116d ago". */
22
+ readonly age?: string;
23
+ readonly title: string;
24
+ readonly detail?: string;
25
+ readonly tone?: WalkthroughTone;
26
+ /** Seen in the record (`"verified"`) or worked out from behaviour (`"inferred"`). */
27
+ readonly provenance?: "verified" | "inferred";
28
+ /** Where it was seen or inferred from: `"git"` shows as "verified · git". */
29
+ readonly source?: string;
30
+ readonly evidence?: readonly WalkthroughEvidence[];
31
+ /** A step it's about: its key, id or number ("1.2"). */
32
+ readonly step?: string;
33
+ /** What happened, which shapes its dot: a release a diamond, an audit a square. */
34
+ readonly type?: "commit" | "release" | "channel" | "audit" | (string & {});
35
+ }
36
+ export interface WalkthroughHistoryGroup {
37
+ readonly heading?: string;
38
+ readonly summary?: string;
39
+ readonly events?: readonly WalkthroughHistoryEvent[];
40
+ /** Another name for `events`. */
41
+ readonly rows?: readonly WalkthroughHistoryEvent[];
42
+ }
43
+ /** One PR in a `stack`. */
44
+ export interface WalkthroughStackItem {
45
+ /** "#57148" (or 57148). */
46
+ readonly pr: string | number;
47
+ readonly href?: string;
48
+ readonly title: string;
49
+ /** Its branch: `"head → base"` (or `->`), or just the head (on `base`, else the trunk). */
50
+ readonly branch?: string;
51
+ /** The branch it merges into, when `branch` names only its head. */
52
+ readonly base?: string;
53
+ /** Lines changed: `"+164 / -18"`, or `{ add, del }`. */
54
+ readonly delta?: string | {
55
+ readonly add: number;
56
+ readonly del: number;
57
+ };
58
+ readonly status?: string;
59
+ readonly tone?: WalkthroughTone;
60
+ readonly summary?: string;
61
+ /** Its head branch, with `base`: another way to write `branch`. */
62
+ readonly head?: string;
63
+ /** The step that shows it: its key, id or number. */
64
+ readonly step?: string;
65
+ /** Another name for `step`. */
66
+ readonly stepId?: string;
67
+ /** Its place as written: "01", "06 · 1/4". */
68
+ readonly order?: string | number;
69
+ }
70
+ /** A table's cell: its text, or that with a link, code, a badge, a colour or a step. */
71
+ export type WalkthroughTableCell = string | number | {
72
+ readonly text: string | number;
73
+ readonly href?: string;
74
+ readonly code?: boolean;
75
+ /** Drawn as a chip in this colour. */
76
+ readonly badge?: WalkthroughTone;
77
+ /** The text in this colour. */
78
+ readonly tone?: WalkthroughTone;
79
+ readonly step?: string;
80
+ };
81
+ export interface WalkthroughTableColumn {
82
+ readonly key: string;
83
+ readonly label: string;
84
+ readonly mono?: boolean;
85
+ /** Numbers go right by default. */
86
+ readonly align?: "left" | "right" | "center";
87
+ }
88
+ /** A row: its cells by column key, bare or under `cells`. */
89
+ export type WalkthroughTableRow = Readonly<Record<string, WalkthroughTableCell>> | {
90
+ readonly cells: Readonly<Record<string, WalkthroughTableCell>>;
91
+ };
92
+ /** A stretch of a video: a pill under it, and a marker on its timeline. */
93
+ export interface WalkthroughChapter {
94
+ readonly id?: string;
95
+ readonly label: string;
96
+ /** Seconds. */
97
+ readonly start: number;
98
+ /** Seconds; else `start + duration`, else the next chapter's start. */
99
+ readonly end?: number;
100
+ readonly duration?: number;
101
+ }
10
102
  /**
11
103
  * What a step shows. A bare string is a video or image URL (by its extension). `mount` is the one
12
104
  * kind that isn't JSON: anything of your own, given its element, returning its cleanup.
@@ -21,12 +113,37 @@ export type WalkthroughMedia = string | {
21
113
  readonly aspect?: number | string;
22
114
  /** The frame strip over the timeline. */
23
115
  readonly strip?: boolean;
116
+ /** Pills under it that seek, and markers on its timeline. */
117
+ readonly chapters?: readonly WalkthroughChapter[];
24
118
  } | {
25
119
  readonly kind: "video";
26
120
  readonly src: string;
27
121
  readonly label?: string;
28
122
  readonly aspect?: number | string;
29
123
  readonly poster?: string;
124
+ /** Pills under it that seek; the one playing lit. */
125
+ readonly chapters?: readonly WalkthroughChapter[];
126
+ /** A WebVTT file's URL, shown by default. */
127
+ readonly captions?: string;
128
+ /** Another name for `captions`. */
129
+ readonly captionsSrc?: string;
130
+ } | {
131
+ readonly kind: "history";
132
+ /** Dated events under quiet headings… */
133
+ readonly groups?: readonly WalkthroughHistoryGroup[];
134
+ /** …or one run of them. */
135
+ readonly events?: readonly WalkthroughHistoryEvent[];
136
+ } | {
137
+ readonly kind: "stack";
138
+ /** The branch the stack lands on ("dev"); else the one most items merge into. */
139
+ readonly base?: string;
140
+ /** In merge order; an item on another's branch hangs under it. */
141
+ readonly items: readonly WalkthroughStackItem[];
142
+ } | {
143
+ readonly kind: "table";
144
+ readonly columns: readonly WalkthroughTableColumn[];
145
+ readonly rows: readonly WalkthroughTableRow[];
146
+ readonly caption?: string;
30
147
  } | {
31
148
  readonly kind: "image";
32
149
  readonly src: string;
@@ -154,11 +271,78 @@ export interface WalkthroughDoc {
154
271
  type CompareMedia = Extract<WalkthroughMedia, {
155
272
  kind: "compare";
156
273
  }>;
157
- /** Media filled in: no bare URLs, and a compare's sides as sources. */
158
- export type WalkMedia = Exclude<WalkthroughMedia, string | CompareMedia> | (Omit<CompareMedia, "before" | "after"> & {
274
+ type VideoMedia = Extract<WalkthroughMedia, {
275
+ kind: "video";
276
+ }>;
277
+ /** A chapter filled in: an id, and its end where one can be known. */
278
+ export interface WalkChapter {
279
+ readonly id: string;
280
+ readonly label: string;
281
+ readonly start: number;
282
+ readonly end?: number;
283
+ }
284
+ export interface WalkHistoryGroup {
285
+ readonly heading?: string;
286
+ readonly summary?: string;
287
+ readonly events: readonly WalkthroughHistoryEvent[];
288
+ }
289
+ /** A stack's item filled in: its PR as `#123`, its branch split, `step` for `stepId`. */
290
+ export interface WalkStackItem {
291
+ readonly pr: string;
292
+ readonly href?: string;
293
+ readonly title: string;
294
+ readonly head?: string;
295
+ readonly base?: string;
296
+ readonly delta?: string | {
297
+ readonly add: number;
298
+ readonly del: number;
299
+ };
300
+ readonly status?: string;
301
+ readonly tone?: WalkthroughTone;
302
+ readonly summary?: string;
303
+ readonly step?: string;
304
+ readonly order?: string;
305
+ }
306
+ export interface WalkTableCell {
307
+ readonly text: string;
308
+ readonly href?: string;
309
+ readonly code?: boolean;
310
+ readonly badge?: WalkthroughTone;
311
+ readonly tone?: WalkthroughTone;
312
+ readonly step?: string;
313
+ }
314
+ export interface WalkTableColumn {
315
+ readonly key: string;
316
+ readonly label: string;
317
+ readonly mono?: boolean;
318
+ readonly align: "left" | "right" | "center";
319
+ }
320
+ /**
321
+ * Media filled in: no bare URLs, a compare's sides as sources, chapters with their ends, history in
322
+ * groups, a stack's branches split and a table's cells as objects. Filling it again changes
323
+ * nothing.
324
+ */
325
+ export type WalkMedia = Exclude<WalkthroughMedia, string | {
326
+ readonly kind: "compare" | "video" | "history" | "stack" | "table";
327
+ }> | (Omit<CompareMedia, "before" | "after" | "chapters"> & {
159
328
  readonly before: WalkthroughSource;
160
329
  readonly after: WalkthroughSource;
161
- });
330
+ readonly chapters?: readonly WalkChapter[];
331
+ }) | (Omit<VideoMedia, "chapters" | "captionsSrc"> & {
332
+ readonly chapters?: readonly WalkChapter[];
333
+ }) | {
334
+ readonly kind: "history";
335
+ readonly groups: readonly WalkHistoryGroup[];
336
+ } | {
337
+ readonly kind: "stack";
338
+ readonly base?: string;
339
+ readonly items: readonly WalkStackItem[];
340
+ } | {
341
+ readonly kind: "table";
342
+ readonly columns: readonly WalkTableColumn[];
343
+ readonly rows: readonly Readonly<Record<string, WalkTableCell>>[];
344
+ readonly caption?: string;
345
+ };
162
346
  export interface WalkOption {
163
347
  readonly id: string;
164
348
  readonly label: string;
@@ -231,6 +415,54 @@ export declare const mediaOf: (src: string) => WalkMedia;
231
415
  export declare function parseAspect(aspect: number | string | undefined): number | undefined;
232
416
  /** Media filled in: a bare URL as a video or image, sources resolved against `base`. */
233
417
  export declare function fillMedia(m: WalkthroughMedia, base?: string): WalkMedia;
418
+ /** `"head → base"` (or `->`, `=>`, `⇒`) as its two branches; one name is the head alone. */
419
+ export declare function parseBranch(branch: string | undefined): {
420
+ head?: string;
421
+ base?: string;
422
+ };
423
+ /** `"+164 / -18"`, `"+164 −18"` or `{ add, del }` as numbers; other text stays as written. */
424
+ export declare function parseDelta(delta: WalkthroughStackItem["delta"]): {
425
+ add: number;
426
+ del: number;
427
+ } | string | undefined;
428
+ /** One row of a stack as drawn: where it hangs, and which rails pass beside it. */
429
+ export interface StackRow {
430
+ readonly item: WalkStackItem;
431
+ /** Its place in `items`. */
432
+ readonly index: number;
433
+ /** 0 on the trunk; one more for each item it's stacked on. */
434
+ readonly depth: number;
435
+ /** The item it's stacked on (its place in `items`); null on the trunk. */
436
+ readonly parent: number | null;
437
+ /** The last of its siblings: its rail ends here (└) rather than going on (├). */
438
+ readonly last: boolean;
439
+ /** For each level above its own (0 the trunk's), whether that level's rail passes beside it. */
440
+ readonly rails: readonly boolean[];
441
+ readonly children: number;
442
+ /** The branch it merges into when that's neither the trunk nor another item's. */
443
+ readonly offBase?: string;
444
+ }
445
+ export interface StackLayout {
446
+ /** The branch it lands on: `base`, else the one most items merge into; null when none says. */
447
+ readonly trunk: string | null;
448
+ /** Depth-first in merge order: each item after the one it's stacked on. */
449
+ readonly rows: readonly StackRow[];
450
+ /** Nothing is stacked on anything, and there's no trunk: a plain list. */
451
+ readonly flat: boolean;
452
+ }
453
+ /** A stack's branch topology: which item each one is stacked on, in rows to draw as a graph. */
454
+ export declare function stackLayout(m: {
455
+ readonly base?: string;
456
+ readonly items: readonly WalkStackItem[];
457
+ }): StackLayout;
458
+ /** Chapters with ids and, where it can be known, an end: given, `start + duration`, or the next. */
459
+ export declare function fillChapters(chapters: readonly WalkthroughChapter[]): WalkChapter[];
460
+ /** The chapter playing at `time` (seconds): the one it's in that started last; -1 for none. */
461
+ export declare function chapterAt(chapters: readonly WalkChapter[], time: number): number;
462
+ /** Seconds as a clock: `1:05`, `1:02:05` past the hour. */
463
+ export declare function clock(seconds: number): string;
464
+ /** A step named by its key, id or number ("1.2"), a leading `#` allowed; undefined if there's none. */
465
+ export declare function findStep(steps: readonly WalkStep[], ref: string | undefined): WalkStep | undefined;
234
466
  /** A GitHub PR or issue URL's number as `#123`; else the URL's last part. */
235
467
  export declare function linkLabel(href: string): string;
236
468
  /**
@@ -280,6 +512,8 @@ export interface MediaContext {
280
512
  readonly time?: number;
281
513
  readonly duration?: number;
282
514
  readonly playing?: boolean;
515
+ /** The chapter the time is in, by its label. */
516
+ readonly chapter?: string;
283
517
  /** The loop range, in seconds. */
284
518
  readonly loop?: {
285
519
  readonly start: number;
@@ -1,6 +1,6 @@
1
1
  import { type StoragePort } from "../core/storage.js";
2
2
  import { type UiLook } from "./look.js";
3
- import { type MediaSlot } from "./walkthrough-media.js";
3
+ import { type MediaSlot, type StepLinks } from "./walkthrough-media.js";
4
4
  import { type WalkOption, type WalkPage, type WalkthroughDoc, type WalkthroughFeedback, type WalkthroughOption, type WalkthroughState } from "./walkthrough-model.js";
5
5
  export interface TasteTestOptions extends UiLook {
6
6
  readonly options: readonly (WalkthroughOption | WalkOption)[];
@@ -9,6 +9,8 @@ export interface TasteTestOptions extends UiLook {
9
9
  readonly onPick?: (id: string | null) => void;
10
10
  /** Mount each option's media when near the view (on by default). */
11
11
  readonly lazy?: boolean;
12
+ /** The page's steps, for media that name one. */
13
+ readonly steps?: StepLinks;
12
14
  }
13
15
  export interface TasteTest {
14
16
  readonly element: HTMLElement;