@kitlangton/knobs 0.3.0 → 0.4.0-next.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,169 @@
1
+ import type { TimeView } from "./timeline-model.js";
2
+ export type CompareMode = "side" | "wipe" | "difference" | "flicker";
3
+ export type MediaKind = "video" | "image";
4
+ /** A source's kind: as given, else by its extension (a video's, or an image). */
5
+ export declare function mediaKind(src: string, kind?: MediaKind): MediaKind;
6
+ /** The frame showing at time `t` (whole frames, never negative). */
7
+ export declare const frameOf: (t: number, fps: number) => number;
8
+ /** The last whole frame of `duration` seconds. */
9
+ export declare const lastFrame: (duration: number, fps: number) => number;
10
+ /**
11
+ * The time a video seeks to for frame `f`: its midpoint, `(f + 0.5) / fps`, which never lands on a
12
+ * frame boundary, so two decoders can't round to different frames. Clamped to the last frame.
13
+ */
14
+ export declare function frameMidpoint(f: number, fps: number, duration?: number): number;
15
+ /** Where both videos seek to show time `t`: the midpoint of the frame it falls in. */
16
+ export declare const seekTime: (t: number, fps: number, duration?: number) => number;
17
+ /** Whether two times show the same frame (a seek between them presents nothing new). */
18
+ export declare const sameFrame: (a: number, b: number, fps: number) => boolean;
19
+ export interface SyncAction {
20
+ /** Hard-seek the follower here (seconds), or leave it playing. */
21
+ readonly seekTo: number | null;
22
+ readonly rate: number;
23
+ }
24
+ /**
25
+ * Keeps a playing follower on its master: past `hard` seconds of drift (90 ms) it seeks; otherwise
26
+ * its rate leans against the drift, proportionally (`gain` per second of drift), by at most `max`
27
+ * (±25%), and inside `dead` (1/120 s) it plays at the base rate.
28
+ */
29
+ export declare function syncAction(master: number, follower: number, o?: {
30
+ rate?: number;
31
+ hard?: number;
32
+ gain?: number;
33
+ max?: number;
34
+ dead?: number;
35
+ }): SyncAction;
36
+ /**
37
+ * One video's seeks, coalesced: while one is in flight only the latest target is kept, applied when
38
+ * it lands; holding a step key walks frames instead of queueing seeks.
39
+ */
40
+ export interface SeekState {
41
+ readonly seeking: boolean;
42
+ /** The latest target asked for while a seek was in flight. */
43
+ readonly pending: number | null;
44
+ }
45
+ export declare const SEEK_IDLE: SeekState;
46
+ export interface SeekStep {
47
+ readonly state: SeekState;
48
+ /** Set `currentTime` to this now, or nothing. */
49
+ readonly apply: number | null;
50
+ /** The frame asked for is already showing: no seek, and no frame callback will come. */
51
+ readonly settled: boolean;
52
+ }
53
+ /** A seek to `t` asked for while the video shows `current`. */
54
+ export declare function requestSeek(s: SeekState, t: number, current: number, fps: number): SeekStep;
55
+ /** The seek in flight landed on `current`: apply what's pending, if it's another frame. */
56
+ export declare function seeked(s: SeekState, current: number, fps: number): SeekStep;
57
+ export interface Rect {
58
+ readonly x: number;
59
+ readonly y: number;
60
+ readonly w: number;
61
+ readonly h: number;
62
+ }
63
+ export interface Size {
64
+ readonly w: number;
65
+ readonly h: number;
66
+ }
67
+ /** `media` fitted inside `box`, centred: the whole of it shows, letterboxed, never cropped. */
68
+ export declare function letterbox(media: Size, box: Rect): Rect;
69
+ export interface CompareLayout {
70
+ /**
71
+ * Side by side: a split screen down the middle, the pair fitted as a whole (After's shape, two
72
+ * up), each side in its half; `split` is the middle's x.
73
+ */
74
+ readonly side: {
75
+ readonly before: Rect;
76
+ readonly after: Rect;
77
+ readonly split: number;
78
+ };
79
+ /** Overlaid (wipe, difference, flicker): both in the same place. */
80
+ readonly overlay: {
81
+ readonly before: Rect;
82
+ readonly after: Rect;
83
+ };
84
+ }
85
+ /** Where the two sides sit inside `box`, side by side and overlaid. */
86
+ export declare function compareLayout(box: Rect, before: Size, after: Size): CompareLayout;
87
+ /** The height a split screen of `w` needs so neither side has margins: half its width, in shape. */
88
+ export declare const splitHeight: (w: number, media: Size) => number;
89
+ /**
90
+ * The part of each side shown, in that side's own 0–1 coordinates: `w` and `h` are equal (1 is
91
+ * the whole picture, 0.25 is 400%). Both sides show the same part.
92
+ */
93
+ export interface ZoomRegion {
94
+ readonly u: number;
95
+ readonly v: number;
96
+ readonly w: number;
97
+ readonly h: number;
98
+ }
99
+ export declare const ZOOM_FIT: ZoomRegion;
100
+ /** 1600% at most. */
101
+ export declare const ZOOM_MAX = 16;
102
+ /** Kept inside the picture, between the whole of it and `ZOOM_MAX`. */
103
+ export declare function clampZoom(r: {
104
+ u: number;
105
+ v: number;
106
+ w: number;
107
+ }): ZoomRegion;
108
+ /** Zoomed by `factor` (above 1 in) about a point at `pu`, `pv` (0–1 across the shown part). */
109
+ export declare function zoomAbout(r: ZoomRegion, factor: number, pu: number, pv: number): ZoomRegion;
110
+ /** Panned by a hand's move of `dx`, `dy` px over a side drawn `base` big: the picture follows it. */
111
+ export declare const panBy: (r: ZoomRegion, dx: number, dy: number, base: Rect) => ZoomRegion;
112
+ /** Where a side drawn in `base` lands when zoomed to `r` (clipped to `base` when drawn). */
113
+ export declare const zoomedRect: (base: Rect, r: ZoomRegion) => Rect;
114
+ /** A point's place across `base` (0–1 each way). */
115
+ export declare const pointIn: (base: Rect, x: number, y: number) => {
116
+ pu: number;
117
+ pv: number;
118
+ };
119
+ /** A loop's window around `time`: a fifth of the length, 1–4 s, inside it. */
120
+ export declare function loopAround(time: number, duration: number): {
121
+ start: number;
122
+ end: number;
123
+ };
124
+ export declare const lerpRect: (a: Rect, b: Rect, k: number) => Rect;
125
+ /** The wipe's line across `rect`: Before shows left of it, After right. */
126
+ export declare const wipeX: (rect: Rect, wipe: number) => number;
127
+ /** The wipe a pointer at `x` (less the grab's offset) puts it at. */
128
+ export declare const wipeAt: (x: number, rect: Rect, offset?: number) => number;
129
+ /**
130
+ * A press at `x`: on the line (within `slop` px) it takes hold where it is, so the line doesn't
131
+ * jump under the hand; elsewhere the line comes to the pointer.
132
+ */
133
+ export declare function wipeGrab(x: number, rect: Rect, wipe: number, slop?: number): {
134
+ onLine: boolean;
135
+ offset: number;
136
+ };
137
+ /** The wipe moved by a key: 1% a press, 10% with Shift. */
138
+ export declare const wipeKey: (wipe: number, dir: -1 | 1, big?: boolean) => number;
139
+ export interface StripColumn {
140
+ /** The frame it samples: its first, so zooming in by two keeps every other sample. */
141
+ readonly frame: number;
142
+ /** Seconds: that frame's midpoint. */
143
+ readonly time: number;
144
+ /** Its first frame and the one after its last. */
145
+ readonly from: number;
146
+ readonly to: number;
147
+ readonly x: number;
148
+ readonly w: number;
149
+ }
150
+ /**
151
+ * The frame strip's columns across `width` px of the timeline's view: each spans a power-of-two
152
+ * number of frames, at least `minWidth` px wide, on a grid fixed to frame 0, sampling its first
153
+ * frame, so panning reuses the samples it has and zooming in by two keeps half of them.
154
+ */
155
+ export declare function stripColumns(view: TimeView, width: number, o: {
156
+ duration: number;
157
+ fps: number;
158
+ minWidth: number;
159
+ }): StripColumn[];
160
+ /** Mean absolute difference of two RGBA buffers' colour, 0 (same) to 1. */
161
+ export declare function meanAbsDiff(a: ArrayLike<number>, b: ArrayLike<number>): number;
162
+ /**
163
+ * The columns worth a marker: where the difference peaks (no less than either neighbour), above
164
+ * `threshold` and at least `ratio` of the largest.
165
+ */
166
+ export declare function stripPeaks(diffs: readonly (number | undefined)[], o?: {
167
+ threshold?: number;
168
+ ratio?: number;
169
+ }): number[];
@@ -0,0 +1,29 @@
1
+ export interface Mp4Sample {
2
+ readonly offset: number;
3
+ readonly size: number;
4
+ /** Seconds, after the edit list: when it shows. */
5
+ readonly pts: number;
6
+ readonly duration: number;
7
+ readonly key: boolean;
8
+ }
9
+ export interface Mp4Track {
10
+ /** A WebCodecs codec string (`avc1.64001f`, `hvc1.1.6.L93.B0`). */
11
+ readonly codec: string;
12
+ readonly description: Uint8Array | undefined;
13
+ readonly width: number;
14
+ readonly height: number;
15
+ /** In decode order. */
16
+ readonly samples: readonly Mp4Sample[];
17
+ }
18
+ /** Parses `buf` (a whole file); null when it isn't a plain MP4 with a video track it knows. */
19
+ export declare function demuxMp4(buf: ArrayBuffer): Mp4Track | null;
20
+ /**
21
+ * The runs to decode for some frames: for each keyframe group (decode order, from a keyframe to
22
+ * the next) holding a wanted presentation index, its sample range. `frameOf` maps a sample's
23
+ * time to the frame it is.
24
+ */
25
+ export declare function decodeRuns(samples: readonly Mp4Sample[], wanted: ReadonlySet<number>, frameOf: (pts: number) => number): {
26
+ from: number;
27
+ to: number;
28
+ frames: number[];
29
+ }[];
@@ -0,0 +1,28 @@
1
+ import type { CompareSource } from "./compare.js";
2
+ import { type UiLook } from "./look.js";
3
+ import type { Timeline } from "./timeline.js";
4
+ export interface CompareStripOptions extends UiLook {
5
+ readonly before: CompareSource;
6
+ readonly after: CompareSource;
7
+ /** The time axis: its view, time and duration; a press here seeks it. */
8
+ readonly timeline: Timeline;
9
+ /** The timeline's by default. */
10
+ readonly fps?: number;
11
+ /** Columns under the timeline's track (`"track"`, the default) or across the strip's own width. */
12
+ readonly align?: "track" | "self";
13
+ /** Differences below this (0–1) never peak. */
14
+ readonly threshold?: number;
15
+ /** A press or drag picked a time. */
16
+ onPick?(time: number): void;
17
+ }
18
+ export interface CompareStrip {
19
+ readonly element: HTMLElement;
20
+ /** Each column's difference (by the frame it samples), 0 (same) to 1. */
21
+ readonly diffs: ReadonlyMap<number, number>;
22
+ update(next: {
23
+ before?: CompareSource;
24
+ after?: CompareSource;
25
+ }): void;
26
+ dispose(): void;
27
+ }
28
+ export declare function compareStrip(container: HTMLElement, options: CompareStripOptions): CompareStrip;
@@ -0,0 +1,103 @@
1
+ import { type CompareMode, type MediaKind, type ZoomRegion } from "./compare-model.js";
2
+ import { type CompareStrip } from "./compare-strip.js";
3
+ import { type UiLook } from "./look.js";
4
+ import { type Timeline } from "./timeline.js";
5
+ export type { CompareMode, MediaKind, ZoomRegion } from "./compare-model.js";
6
+ /** A loop's in and out (seconds). */
7
+ export interface CompareLoop {
8
+ readonly start: number;
9
+ readonly end: number;
10
+ }
11
+ /** What's in view: the zoomed part (both sides) and the loop, for a log or a note. */
12
+ export interface CompareView {
13
+ readonly zoom: ZoomRegion;
14
+ readonly loop: CompareLoop | null;
15
+ }
16
+ /**
17
+ * How to encode recordings for review: constant frame rate and a keyframe every 15 frames, so a
18
+ * seek lands on the exact frame almost at once. Raw screen recordings often have one keyframe in
19
+ * 8–18 s; the viewer copes (it shows decoded frames while seeks land), but this makes it exact.
20
+ */
21
+ export declare const COMPARE_FFMPEG = "ffmpeg -i in.mov -vf fps=30 -fps_mode cfr -c:v libx264 -preset slow -crf 18 -g 15 -keyint_min 15 -pix_fmt yuv420p -movflags +faststart -an out.mp4";
22
+ export interface CompareSource {
23
+ readonly src: string;
24
+ /** From the extension when left out (`.mp4`, `.webm`, `.mov`… are videos). */
25
+ readonly kind?: MediaKind;
26
+ /** "Before" and "After" by default. */
27
+ readonly label?: string;
28
+ }
29
+ export interface CompareOptions extends UiLook {
30
+ readonly before: CompareSource;
31
+ readonly after: CompareSource;
32
+ /** `"side"` (side by side) by default. */
33
+ readonly mode?: CompareMode;
34
+ /** Where the wipe's line sits, 0 (all After) to 1 (all Before); 0.5. */
35
+ readonly wipe?: number;
36
+ /** Frames per second (the bound timeline's, else 30): steps, and where paused videos land. */
37
+ readonly fps?: number;
38
+ /** The loupe under the pointer (`Z` held shows it too). */
39
+ readonly loupe?: boolean;
40
+ /** Difference mode hides changes below this (0–1, 4/255): codec noise. */
41
+ readonly threshold?: number;
42
+ /**
43
+ * The clock: an existing `timeline()` (its time drives both sides; while playing it follows
44
+ * After), or `true` for one of its own under the viewer. Without one, `seek`, `play` and keys
45
+ * still work.
46
+ */
47
+ readonly timeline?: Timeline | boolean;
48
+ /** The frame strip over the timeline (off; one timeline is made if there's none). */
49
+ readonly strip?: boolean;
50
+ /**
51
+ * The sources' width over height before they load (16/9). In a container without a height of
52
+ * its own, the viewer takes the height its sides need: a split screen with no margins.
53
+ */
54
+ readonly aspect?: number;
55
+ /** Load sources only near the screen and let them go well off it (true). */
56
+ readonly lazy?: boolean;
57
+ /** `"gpu"` (the default, where WebGPU is there) or `"dom"`. */
58
+ readonly renderer?: "gpu" | "dom";
59
+ readonly label?: string;
60
+ /** The modes, the loupe and the wipe take codes in the page's hint mode (`s`); on by default. */
61
+ readonly hints?: boolean;
62
+ onMode?(mode: CompareMode): void;
63
+ /** The wipe moved: dragged, keyed or set. */
64
+ onWipe?(wipe: number): void;
65
+ /** The zoom or the loop changed (at most once a frame). */
66
+ onView?(view: CompareView): void;
67
+ }
68
+ export interface Compare {
69
+ readonly element: HTMLElement;
70
+ readonly mode: CompareMode;
71
+ readonly wipe: number;
72
+ readonly loupe: boolean;
73
+ /** Seconds. */
74
+ readonly time: number;
75
+ readonly playing: boolean;
76
+ /** The longer video's, in seconds (0 for two images). */
77
+ readonly duration: number;
78
+ readonly renderer: "gpu" | "dom";
79
+ /** The bound timeline, or the one it made. */
80
+ readonly timeline: Timeline | null;
81
+ readonly strip: CompareStrip | null;
82
+ /** The part of each side in view (`{ u: 0, v: 0, w: 1, h: 1 }` is all of it). */
83
+ readonly zoom: ZoomRegion;
84
+ /** The timeline's loop, or null. */
85
+ readonly loop: CompareLoop | null;
86
+ /** New sources, mode, wipe, loupe, threshold or fps. A new mode glides in. */
87
+ update(next: Partial<Pick<CompareOptions, "before" | "after" | "mode" | "wipe" | "loupe" | "threshold" | "fps">>): void;
88
+ seek(time: number): void;
89
+ /** Moves by `seconds`, the timeline's playhead gliding there (playing carries on). */
90
+ seekBy(seconds: number): void;
91
+ play(): void;
92
+ pause(): void;
93
+ toggle(): void;
94
+ /** Steps whole frames (pausing first). */
95
+ step(frames: number): void;
96
+ /** Zooms both sides to `region` (null: all of it), gliding there unless `animate` is false. */
97
+ setZoom(region: ZoomRegion | null, animate?: boolean): void;
98
+ /** A loop around the playhead (`true`), a given one, or none; playback stays inside it. */
99
+ setLoop(loop: CompareLoop | boolean): void;
100
+ focus(): void;
101
+ dispose(): void;
102
+ }
103
+ export declare function compare(container: HTMLElement, options: CompareOptions): Compare;
@@ -0,0 +1 @@
1
+ export declare const diffCss: string;
@@ -0,0 +1,128 @@
1
+ /** A file as before and after text. */
2
+ export interface DiffSource {
3
+ /** Its path, which is its id. */
4
+ readonly name: string;
5
+ readonly before: string;
6
+ readonly after: string;
7
+ /** A Shiki language id, when the extension doesn't say. */
8
+ readonly lang?: string;
9
+ }
10
+ /** What a diff takes: a unified patch (`git diff`, `git show`, one file or many) or pairs. */
11
+ export interface DiffInput {
12
+ readonly patch?: string;
13
+ readonly files?: readonly DiffSource[];
14
+ }
15
+ export type DiffChange = "change" | "new" | "deleted" | "rename";
16
+ export interface DiffFile {
17
+ /** Its path after the change (before it, for a deleted file): the file's id. */
18
+ readonly name: string;
19
+ /** Its path before a rename. */
20
+ readonly prevName?: string;
21
+ readonly change: DiffChange;
22
+ readonly additions: number;
23
+ readonly deletions: number;
24
+ readonly binary?: boolean;
25
+ /** Its own part of the patch, headers and hunks. */
26
+ readonly patch?: string;
27
+ readonly before?: string;
28
+ readonly after?: string;
29
+ readonly lang?: string;
30
+ /** The rows its diff takes once drawn (see `DiffRows`). */
31
+ readonly rows?: DiffRows;
32
+ }
33
+ /**
34
+ * How tall a file's diff is once drawn, in rows: each layout's lines (`split` pairs a removed
35
+ * line with an added one) and the separators that stand for unchanged lines between hunks. A
36
+ * card reserves this much before the renderer has loaded, so nothing jumps when it does.
37
+ */
38
+ export interface DiffRows {
39
+ readonly unified: number;
40
+ readonly split: number;
41
+ readonly separators: number;
42
+ }
43
+ /** The context lines a patch made from before and after text keeps around each change. */
44
+ export declare const DIFF_CONTEXT = 3;
45
+ /**
46
+ * The files in a unified patch, in its order, with lines added and removed counted from the
47
+ * hunks. Takes `git diff`, `git show` and `format-patch` output (anything before the first
48
+ * file, a commit's header and message, is skipped) and plain `diff -u` output.
49
+ */
50
+ export declare function parsePatch(patch: string): DiffFile[];
51
+ /** Lines one side has that the other doesn't, by a shortest edit (Myers), as a patch counts them. */
52
+ export declare function countChanges(before: string, after: string): {
53
+ additions: number;
54
+ deletions: number;
55
+ };
56
+ /** What a patch made from before and after text would show: its counts and its rows. */
57
+ export declare function compareText(before: string, after: string, context?: number): {
58
+ additions: number;
59
+ deletions: number;
60
+ rows: DiffRows;
61
+ };
62
+ /**
63
+ * The rows of a patch over whole files: changes with `context` unchanged lines around them,
64
+ * changes no more than twice that apart in one hunk, and a separator for each run of
65
+ * unchanged lines left out (before, between and after the hunks).
66
+ */
67
+ export declare function hunkRows(ops: ArrayLike<number>, context?: number): DiffRows;
68
+ /**
69
+ * A fingerprint of a file's change (its part of the patch, or its before and after text), so a
70
+ * file marked viewed comes back unviewed once its diff changes.
71
+ */
72
+ export declare function fileKey(file: DiffFile): string;
73
+ /**
74
+ * Orders paths as a file tree shows them: at each level folders first, then files, each by
75
+ * name (natural order, so `file2` comes before `file10`).
76
+ */
77
+ export declare function comparePaths(a: string, b: string): number;
78
+ /** A diff's files in tree order, counted: from the patch, or from each pair's text. */
79
+ export declare function diffFiles(input: DiffInput): DiffFile[];
80
+ /** Lines added and removed across the files. */
81
+ export declare function diffStats(files: readonly DiffFile[]): {
82
+ files: number;
83
+ additions: number;
84
+ deletions: number;
85
+ };
86
+ /** A path as the folder it's in and its own name. */
87
+ export declare function splitPath(name: string): {
88
+ dir: string;
89
+ base: string;
90
+ };
91
+ /** A row of a diff's file tree: a folder (one or more path segments) or a file. */
92
+ export interface DiffTreeItem {
93
+ /** A file's name, or `dir:` and a folder's path. */
94
+ readonly id: string;
95
+ readonly label: string;
96
+ /** The folder's path, or the file's. */
97
+ readonly path: string;
98
+ readonly file?: DiffFile;
99
+ readonly children?: readonly DiffTreeItem[];
100
+ }
101
+ /**
102
+ * The files as tree items: folders hold what's in them, and a folder whose only child is a
103
+ * folder joins it (`src` › `ui` is one row, `src/ui`), so short trees stay flat.
104
+ */
105
+ export declare function diffTree(files: readonly DiffFile[]): DiffTreeItem[];
106
+ /** Every folder's id in a diff tree (to open them all). */
107
+ export declare function diffFolders(items: readonly DiffTreeItem[]): string[];
108
+ /**
109
+ * Which file to show once the list changes: the same one if it's still there; else the one now
110
+ * where it was (the next, or the last); else the first. Null with no files.
111
+ */
112
+ export declare function keepFile(prev: readonly DiffFile[], next: readonly DiffFile[], current: string | null | undefined): string | null;
113
+ /** The file before or after `current`, stopping at the ends. */
114
+ export declare function stepFile(files: readonly DiffFile[], current: string | null | undefined, dir: -1 | 1): string | null;
115
+ /**
116
+ * A change as a bar of `cells` squares, as GitHub draws one: how many show added, how many
117
+ * removed and how many neither. A change smaller than the bar fills one square a line; each side
118
+ * that has lines keeps at least one.
119
+ */
120
+ export declare function diffBar(additions: number, deletions: number, cells?: number): [number, number, number];
121
+ /** Whether a path has every word of a query in it, any case: `ui diff` finds `src/ui/diff.ts`. */
122
+ export declare function matchPath(path: string, query: string): boolean;
123
+ /** The files still viewed: those saved viewed whose change is the one saved (see `fileKey`). */
124
+ export declare function stillViewed(saved: Readonly<Record<string, string>>, files: readonly DiffFile[]): Set<string>;
125
+ /** What to save for the files viewed: each one's name and the key of its change. */
126
+ export declare function saveViewed(viewed: Iterable<string>, files: readonly DiffFile[]): Record<string, string>;
127
+ /** The files under a tree item (itself, for a file). */
128
+ export declare function filesUnder(item: DiffTreeItem): string[];
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The CSS to hand the renderer: its code columns' native scrollbars hidden (`codeBars` draws
3
+ * thin ones), and every corner its stylesheet rounds by 6px rounded by `--kd-inner` instead, so
4
+ * a hunk separator inset in the card is concentric with it. Read once from its stylesheet.
5
+ */
6
+ export declare function rendererCss(coreCss: () => string): string;
7
+ export interface CodeBars {
8
+ /** Looks for the code columns again (after a render) and places their bars. */
9
+ rescan(): void;
10
+ place(): void;
11
+ dispose(): void;
12
+ }
13
+ /**
14
+ * Thin bars for the code columns' sideways scroll (one per column; two when split), drawn in
15
+ * `layer` (a positioned element inside the scroller) at the bottom of what shows of each
16
+ * column, so the bar is in view however far down a long file you are. Hidden until the pointer
17
+ * is over the diff, or while you scroll the code sideways or drag a bar, as `overlayScrollbar`'s.
18
+ */
19
+ export declare function codeBars(scroller: HTMLElement, layer: HTMLElement, root: () => ShadowRoot | null | undefined): CodeBars;
20
+ export interface EdgeFades {
21
+ update(): void;
22
+ dispose(): void;
23
+ }
24
+ /**
25
+ * Marks a scroller `data-fade-top` once it's scrolled and `data-fade-bottom` while there's more
26
+ * below, for the CSS that fades its content out at that edge (a mask, its stops on registered
27
+ * properties so they ease in and out).
28
+ */
29
+ export declare function edgeFades(scroller: HTMLElement, content: Element): EdgeFades;
@@ -0,0 +1,158 @@
1
+ import { type StoragePort } from "../core/storage.js";
2
+ import { type DiffFile, type DiffSource, type DiffTreeItem } from "./diff-model.js";
3
+ import { type UiLook } from "./look.js";
4
+ import { type Tree } from "./tree.js";
5
+ export type DiffLayout = "split" | "unified";
6
+ /** Lines picked in a diff (`onLines`): a range on one side, or across both. */
7
+ export interface DiffLines {
8
+ readonly start: number;
9
+ readonly end: number;
10
+ readonly side?: "deletions" | "additions";
11
+ readonly endSide?: "deletions" | "additions";
12
+ }
13
+ export interface CodeDiffOptions extends UiLook {
14
+ /** A unified patch: `git diff` or `git show` output, one file or many. */
15
+ readonly patch?: string;
16
+ /** Or files as before and after text (`patch` wins when both are set). */
17
+ readonly files?: readonly DiffSource[];
18
+ /** The file on show (its path); the first unless set. */
19
+ readonly file?: string | null;
20
+ /** Side by side (default) or one column. */
21
+ readonly layout?: DiffLayout;
22
+ /** Long lines wrap instead of scrolling sideways. */
23
+ readonly wrap?: boolean;
24
+ /**
25
+ * `"fill"` (default): the card fills its container and the diff scrolls inside it. `"auto"`:
26
+ * the card is as tall as the file's diff, up to `maxHeight`, and springs to each new height;
27
+ * it holds the height the diff will take from the start, so a page doesn't jump as it loads.
28
+ */
29
+ readonly height?: "fill" | "auto";
30
+ /** Pixels, for `height: "auto"` (640, or 70% of the window when that's less); it scrolls past. */
31
+ readonly maxHeight?: number;
32
+ /**
33
+ * Draws only near the viewport (a screen ahead) and lets go of a diff far behind, keeping its
34
+ * height; on by default with `height: "auto"`, for a long page of them. `ready` waits for it.
35
+ */
36
+ readonly lazy?: boolean;
37
+ /**
38
+ * Every file down one scroller, each under its own header (status, path, counts, Viewed)
39
+ * that sticks to the top until the next file's pushes it off; ‹ › and `show()` glide to a
40
+ * file, and the file on show follows the scroll. Each file draws when it's near the view.
41
+ */
42
+ readonly stack?: boolean;
43
+ /** The files marked viewed; with this or `onViewed`, a Viewed check in the header. */
44
+ readonly viewed?: readonly string[];
45
+ /** The header's Viewed check marked or cleared the file on show. */
46
+ readonly onViewed?: (viewed: readonly string[], file: string) => void;
47
+ /** ‹ 2 of 5 › in the header when there are several files (on by default). */
48
+ readonly nav?: boolean;
49
+ /** Shiki themes for the code, dark and light (Pierre's own unless set). */
50
+ readonly syntax?: {
51
+ readonly dark: string;
52
+ readonly light: string;
53
+ };
54
+ /** Its own controls changed the file (‹ ›); `show()` doesn't call it. */
55
+ readonly onFile?: (name: string) => void;
56
+ readonly onLayout?: (layout: DiffLayout) => void;
57
+ readonly onWrap?: (wrap: boolean) => void;
58
+ /** Lines picked by dragging over line numbers; turns line selection on. */
59
+ readonly onLines?: (lines: DiffLines | null, file: string) => void;
60
+ /** Shown with no files. */
61
+ readonly empty?: string;
62
+ /** An element of your own at the start of the header (diffReview puts its list's toggle here). */
63
+ readonly lead?: HTMLElement;
64
+ }
65
+ export interface CodeDiffUpdate {
66
+ readonly patch?: string;
67
+ readonly files?: readonly DiffSource[];
68
+ readonly file?: string | null;
69
+ readonly layout?: DiffLayout;
70
+ readonly wrap?: boolean;
71
+ readonly viewed?: readonly string[];
72
+ /**
73
+ * Switches between filling the container and its diff's own height, in place: the views and
74
+ * their scroll stay. The new height is taken as it is, not sprung, so a container whose size
75
+ * is moving (a card going full window) is followed rather than fought.
76
+ */
77
+ readonly height?: "fill" | "auto";
78
+ readonly maxHeight?: number;
79
+ }
80
+ export interface CodeDiff {
81
+ readonly element: HTMLElement;
82
+ /** The diff's files, counted, in tree order. */
83
+ readonly files: readonly DiffFile[];
84
+ readonly file: string | null;
85
+ readonly layout: DiffLayout;
86
+ readonly wrap: boolean;
87
+ readonly viewed: readonly string[];
88
+ /** Shows a file: the new one fades in over the last, in place. */
89
+ show(name: string): void;
90
+ /** The file before or after the one on show. */
91
+ step(dir: -1 | 1): void;
92
+ /** New input or settings; a new patch keeps the file on show if it's still there. */
93
+ update(next: CodeDiffUpdate): void;
94
+ /** Settles once the first file is drawn. */
95
+ readonly ready: Promise<void>;
96
+ dispose(): void;
97
+ }
98
+ /**
99
+ * A file's `+12 −3`, coloured, for a row of your own (a tree's `detail`); `bar` adds five
100
+ * squares that show the share added and removed, as GitHub does.
101
+ */
102
+ export declare function diffCounts(file: DiffFile, doc?: Document, options?: {
103
+ readonly bar?: boolean;
104
+ }): HTMLElement;
105
+ /** A file's change as a letter (M, A, D, R), for a tree's `icon`. */
106
+ export declare function diffStatus(file: DiffFile, doc?: Document): HTMLElement;
107
+ /**
108
+ * A code diff in a knobs card: a header with the file's path, its counts and Split | Unified
109
+ * over the diff, drawn by `@pierre/diffs` (Shiki highlighting, word-level changes, hunks that
110
+ * expand when the whole files are known). Takes a unified patch or before and after pairs.
111
+ * Changing the file or the layout fades the new view in over the old, in place; each file
112
+ * keeps its scroll. On another surface (a card, a split's pane, a walkthrough's step) it draws
113
+ * no card of its own. Needs `@pierre/diffs` installed; it loads on first use.
114
+ */
115
+ export declare function codeDiff(container: HTMLElement, options: CodeDiffOptions): CodeDiff;
116
+ export interface DiffReviewOptions extends CodeDiffOptions {
117
+ /**
118
+ * Saves whether the file list is open and the files marked viewed, under this id. A viewed
119
+ * file whose change moves on comes back unviewed.
120
+ */
121
+ readonly id?: string;
122
+ readonly storage?: StoragePort | null;
123
+ /** The file list's title ("Files"). */
124
+ readonly title?: string;
125
+ /** The file list's width in pixels when open (256). */
126
+ readonly listSize?: number;
127
+ /** The file list starts open (the default) or closed; what was last chosen wins, with an id. */
128
+ readonly listOpen?: boolean;
129
+ /** A field over the file list that narrows it by path (on by default). */
130
+ readonly search?: boolean;
131
+ }
132
+ export interface DiffReview {
133
+ readonly element: HTMLElement;
134
+ readonly tree: Tree<DiffTreeItem>;
135
+ readonly diff: CodeDiff;
136
+ /** The files marked viewed. */
137
+ readonly viewed: readonly string[];
138
+ /** Marks files viewed or not (a folder's id marks what's in it). */
139
+ setViewed(names: readonly string[], viewed: boolean): void;
140
+ /** Narrows the file list to paths with every word of `query`. */
141
+ filter(query: string): void;
142
+ /** Whether the file list is open. */
143
+ readonly listOpen: boolean;
144
+ /** Springs the file list open or closed (toggles without an argument). */
145
+ toggleList(open?: boolean): void;
146
+ update(next: CodeDiffUpdate): void;
147
+ dispose(): void;
148
+ }
149
+ /**
150
+ * A diff beside its file list, as a review, in one card: the list is a drawer at its left that
151
+ * springs open and shut (0 to 256px, .4 s, no bounce; the diff reflows as it moves) from a
152
+ * button at the start of the diff's header, or ⌘B / Ctrl+B. In it, a field that narrows the list
153
+ * by path, then the changed files as a compact tree (folders folded into paths), each with its
154
+ * change as a letter (M, A, D, R, coloured), its +/− counts and their bar, and a tick to mark it
155
+ * viewed; one highlight glides to the file on show. Viewed files dim, count in the list's title
156
+ * and, with an `id`, are remembered until their change moves on. `tree` and `diff` are the pieces.
157
+ */
158
+ export declare function diffReview(container: HTMLElement, options: DiffReviewOptions): DiffReview;