@kitlangton/knobs 0.3.0 → 0.4.0-next.1
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 +350 -45
- package/dist/dev/{chunk-jx1ktap3.js → chunk-1qzffbpa.js} +497 -422
- package/dist/dev/{chunk-7xgkerd2.js → chunk-3ygqgy1x.js} +23 -1
- package/dist/dev/{chunk-pmkvb9ep.js → chunk-8mabjeyk.js} +1920 -507
- package/dist/dev/{chunk-v8ssbcbk.js → chunk-ayb292zj.js} +13 -11
- package/dist/dev/index.js +10 -5
- package/dist/index.js +25 -1
- package/dist/panel.js +2954 -1291
- package/dist/react.js +294 -6
- package/dist/solid.js +292 -4
- package/dist/themes.js +1509 -7
- package/dist/types/core/adapter.d.ts +6 -2
- package/dist/types/core/appearance.d.ts +44 -0
- package/dist/types/core/glide.d.ts +7 -0
- package/dist/types/core/motion.d.ts +66 -0
- package/dist/types/core/root.d.ts +13 -0
- package/dist/types/core/store.d.ts +18 -5
- package/dist/types/core/theme.d.ts +10 -3
- package/dist/types/index.d.ts +2 -1
- package/dist/types/panel/gpu/color.d.ts +6 -0
- package/dist/types/panel/motion.d.ts +1 -1
- package/dist/types/panel/styles.d.ts +2 -3
- package/dist/types/themes/define.d.ts +104 -0
- package/dist/types/themes/index.d.ts +11 -0
- package/dist/types/themes/knobs.d.ts +5 -0
- package/dist/types/themes/void.d.ts +8 -0
- package/dist/types/tokens/color.d.ts +42 -0
- package/dist/types/tokens/css.d.ts +13 -0
- package/dist/types/tokens/evaluate.d.ts +8 -0
- package/dist/types/tokens/index.d.ts +4 -0
- package/dist/types/tokens/schema.d.ts +1100 -0
- package/dist/types/ui/bento-css.d.ts +1 -1
- package/dist/types/ui/compare-css.d.ts +1 -0
- package/dist/types/ui/compare-frames.d.ts +41 -0
- package/dist/types/ui/compare-gpu.d.ts +58 -0
- package/dist/types/ui/compare-model.d.ts +169 -0
- package/dist/types/ui/compare-mp4.d.ts +29 -0
- package/dist/types/ui/compare-strip.d.ts +28 -0
- package/dist/types/ui/compare.d.ts +103 -0
- package/dist/types/ui/diff-css.d.ts +1 -0
- package/dist/types/ui/diff-model.d.ts +128 -0
- package/dist/types/ui/diff-scroll.d.ts +29 -0
- package/dist/types/ui/diff.d.ts +158 -0
- package/dist/types/ui/index.d.ts +10 -2
- package/dist/types/ui/look.d.ts +18 -13
- package/dist/types/ui/overlay-css.d.ts +1 -1
- package/dist/types/ui/react.d.ts +66 -0
- package/dist/types/ui/readout-css.d.ts +1 -1
- package/dist/types/ui/readout-model.d.ts +5 -1
- package/dist/types/ui/rgba.d.ts +11 -0
- package/dist/types/ui/scrollbar.d.ts +2 -2
- package/dist/types/ui/solid.d.ts +52 -0
- package/dist/types/ui/timeline.d.ts +35 -2
- package/dist/types/ui/tree.d.ts +7 -2
- package/dist/types/ui/walkthrough-css.d.ts +1 -0
- package/dist/types/ui/walkthrough-media.d.ts +29 -0
- package/dist/types/ui/walkthrough-model.d.ts +447 -0
- package/dist/types/ui/walkthrough.d.ts +63 -0
- package/dist/ui-react.js +18352 -8462
- package/dist/ui-solid.js +17947 -8148
- package/dist/ui.js +19240 -9701
- package/package.json +6 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const bentoCss = "\n.knobs-ui.knobs-bento{--
|
|
1
|
+
export declare const bentoCss = "\n.knobs-ui.knobs-bento{--_ku-bento-radius:var(--k-ui-bento-radius,max(0px,calc(var(--_k-radius) - 5px)));\n position:relative;width:100%;height:100%;overflow:hidden auto;outline:none;background:var(--k-ui-bento-bg,var(--_k-bg-page));\n scrollbar-width:thin;scrollbar-gutter:stable;overscroll-behavior:contain;touch-action:pan-y;user-select:none;-webkit-user-select:none;\n font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}\n.knobs-bento[data-frame=\"flat\"]{--_ku-bento-radius:var(--k-ui-bento-radius,0px)}\n.knobs-bento[data-pending] .knobs-bento-space{visibility:hidden}\n:where(.knobs-bento-space,.knobs-bento-space *){box-sizing:border-box}\n.knobs-bento-space{position:relative;width:100%}\n.knobs-bento-tile{position:absolute;left:0;top:0;transform-origin:0 0;overflow:hidden;border-radius:var(--_ku-bento-radius);\n background:var(--_k-fill);contain:strict;cursor:zoom-in}\n.knobs-bento-tile[data-focus]{z-index:2;cursor:zoom-out}\n.knobs-bento-tile[data-focus][data-live]{cursor:auto}\n.knobs-bento-img{display:block;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .18s cubic-bezier(.2,.8,.2,1);-webkit-user-drag:none}\n.knobs-bento-tile[data-ready] .knobs-bento-img{opacity:1}\n.knobs-bento-tile[data-instant] .knobs-bento-img{transition:none}\n.knobs-bento-body{position:absolute;inset:0;pointer-events:none}\n.knobs-bento-tile[data-focus] .knobs-bento-body{pointer-events:auto}\n.knobs-bento-label{position:absolute;left:9px;right:9px;bottom:7px;color:#fff;font-size:var(--_k-text-meta);line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;\n text-shadow:0 0 6px rgba(0,0,0,.55),0 0 1px rgba(0,0,0,.5);opacity:0;pointer-events:none;transition:opacity calc(var(--_k-hover-out) * 1ms)}\n.knobs-bento:not([data-keyboard]) .knobs-bento-tile:hover .knobs-bento-label,\n.knobs-bento[data-keyboard]:focus .knobs-bento-tile[data-active] .knobs-bento-label,\n.knobs-bento-tile[data-focus] .knobs-bento-label{opacity:1;transition-duration:calc(var(--_k-hover-in) * 1ms)}\n/* A pick: a small dot in the accent, ringed in the page so it reads on any picture. */\n.knobs-bento-tile::after{content:\"\";position:absolute;top:8px;right:8px;width:8px;height:8px;border-radius:50%;background:var(--_k-accent);\n box-shadow:0 0 0 1.5px color-mix(in srgb,var(--_k-bg-page) 70%,transparent);opacity:0;scale:.4;pointer-events:none;\n transition:opacity .11s cubic-bezier(.4,0,1,1),scale var(--_ku-ease-ms) var(--_ku-ease)}\n.knobs-bento-tile[aria-selected=\"true\"]::after{opacity:1;scale:1;transition:opacity .15s cubic-bezier(.2,.8,.2,1),scale var(--_ku-ease-ms) var(--_ku-ease)}\n.knobs-bento-wash{position:absolute;left:0;top:0;width:100px;height:100px;transform-origin:0 0;z-index:3;pointer-events:none;border-radius:var(--_ku-bento-radius);\n background:color-mix(in srgb,var(--_k-accent) 12%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--_k-accent) 45%,transparent);\n opacity:0;transition:opacity .15s}\n.knobs-bento[data-keyboard]:focus:not([data-zoomed]) .knobs-bento-wash[data-on]{opacity:1}\n.knobs-bento-scrim{position:absolute;left:0;top:0;width:100%;z-index:1;background:var(--k-ui-scrim,var(--k-ui-bento-bg,var(--_k-bg-page)));opacity:0;pointer-events:none;cursor:zoom-out}\n.knobs-bento[data-zoomed] .knobs-bento-scrim{pointer-events:auto}\n.knobs-bento-head{position:absolute;left:0;top:0;display:flex;align-items:flex-end;padding:0 0 6px 2px;color:var(--_k-fg-2);font-weight:var(--_k-weight-label);\n font-size:var(--_k-text-meta);white-space:nowrap;pointer-events:none}\n";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const compareCss = "\n:where(.knobs-compare,.knobs-compare *,.knobs-cmp-strip,.knobs-cmp-strip *){box-sizing:border-box}\n.knobs-ui.knobs-compare{--_ku-cmp-diff:var(--k-compare-diff,var(--_k-ok));--_ku-cmp-line:var(--_k-compare-divider);\n --_ku-cmp-stage:var(--_k-compare-stage);\n display:flex;flex-direction:column;gap:6px;width:100%;height:100%;min-height:160px;outline:none;user-select:none;-webkit-user-select:none;\n font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}\n:where(.knobs-ui) :is(.knobs-cmp-mode,.knobs-cmp-toggle){all:revert;box-sizing:border-box;font:inherit;color:inherit;letter-spacing:inherit;background:transparent;border:0;margin:0;padding:0;outline:none;box-shadow:none}\n.knobs-cmp-bar{flex:none;display:flex;align-items:center;gap:2px;height:24px;padding:0 2px}\n.knobs-cmp-modes{display:flex;align-items:center;gap:2px}\n.knobs-ui .knobs-cmp-mode,.knobs-ui .knobs-cmp-toggle{height:22px;padding:0 7px;border-radius:max(0px,calc(var(--_k-radius) - 2px));color:var(--_k-fg-3);cursor:pointer;white-space:nowrap;\n transition:color .15s,background .15s}\n.knobs-ui .knobs-cmp-mode:is(:hover,:focus-visible),.knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){color:var(--_k-fg);background:var(--_ku-wash)}\n.knobs-ui .knobs-cmp-mode[aria-checked=\"true\"],.knobs-ui .knobs-cmp-toggle[aria-pressed=\"true\"]{color:var(--_k-fg);font-weight:var(--_k-weight-label)}\n.knobs-cmp-fill{flex:1}\n.knobs-cmp-toggle[hidden]{display:none}\n/* One surface: on a card (a walkthrough's step, a split's pane, anything .knobs-surface) or marked\n data-nested, the stage drops its fill and the sides sit on that card. */\n:where(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-ui.knobs-compare,.knobs-ui.knobs-compare[data-nested]{--_ku-cmp-stage:var(--k-compare-stage,transparent)}\n/* In a page's flow (a container with no height), it takes the height its sides need. */\n.knobs-ui.knobs-compare[data-auto]{height:auto}\n.knobs-compare[data-auto] .knobs-cmp-stage{flex:none;min-height:0}\n.knobs-cmp-stage{position:relative;flex:1;min-height:96px;overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_ku-cmp-stage);touch-action:pan-y;outline:none}\n.knobs-compare[data-frame=\"flat\"] .knobs-cmp-stage{border-radius:0}\n.knobs-compare[data-mode=\"wipe\"] .knobs-cmp-stage{cursor:ew-resize}\n/* Unzoomed, a finger still scrolls the page up and down; zoomed, the stage takes every gesture. */\n.knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:grab;touch-action:none}\n.knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:grabbing!important}\n.knobs-compare[data-dragging],.knobs-compare[data-dragging] *{cursor:ew-resize!important}\n.knobs-cmp-sizer{position:absolute;left:0;right:0;top:0;height:0;visibility:hidden;pointer-events:none}\n.knobs-cmp-canvas{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}\n.knobs-cmp-media{position:absolute;left:0;top:0;display:block;max-width:none;margin:0;padding:0;border:0;object-fit:fill;transform-origin:0 0;pointer-events:none;will-change:transform}\n.knobs-compare[data-renderer=\"gpu\"] .knobs-cmp-media{opacity:0}\n/* Glass: what's behind shows through, blurred, on the knobs tokens. */\n.knobs-cmp-label,.knobs-cmp-zoom{background:color-mix(in srgb,var(--_k-bg) 52%,transparent);-webkit-backdrop-filter:blur(14px) saturate(1.6);backdrop-filter:blur(14px) saturate(1.6);\n box-shadow:inset 0 0 0 .5px var(--_k-border),inset 0 1px 0 var(--_k-rim),0 2px 10px rgba(0,0,0,calc(.18 * var(--_k-shadow-k,1)))}\n.knobs-cmp-label{position:absolute;left:0;top:0;padding:1px 7px;border-radius:999px;font-size:var(--_k-text-meta);line-height:16px;color:var(--_k-fg-2);white-space:nowrap;pointer-events:none;will-change:transform;transition:color .15s}\n.knobs-cmp-label[data-lit]{color:var(--_k-fg)}\n.knobs-cmp-label[data-dim]{color:var(--_k-fg-3)}\n/* The wipe: a hairline down the line with a grip at its middle; a wider strip takes the hand and\n the keys (a slider). It fades with the mode. */\n.knobs-cmp-handle{cursor:ew-resize;position:absolute;left:-12px;top:0;width:24px;height:0;opacity:0;pointer-events:none;outline:none;will-change:transform;transition:opacity .18s cubic-bezier(.4,0,1,1)}\n.knobs-cmp-handle[data-on]{opacity:1;pointer-events:auto;transition:opacity .22s cubic-bezier(.2,.8,.2,1)}\n.knobs-cmp-handle::before{content:\"\";position:absolute;top:0;bottom:0;left:11.5px;width:1px;background:var(--_ku-cmp-line);box-shadow:0 0 0 .5px rgba(0,0,0,.28),0 0 6px rgba(0,0,0,.25)}\n.knobs-cmp-handle::after{content:\"\";position:absolute;top:50%;left:9px;width:6px;height:28px;margin-top:-14px;border-radius:3px;background:var(--_ku-cmp-line);\n box-shadow:0 1px 3px rgba(0,0,0,.35),0 0 0 .5px rgba(0,0,0,.2);transition:scale .18s var(--_ku-ease),box-shadow .15s}\n.knobs-compare[data-dragging] .knobs-cmp-handle::after{scale:1.12}\n.knobs-cmp-handle:focus-visible::after,.knobs-compare[data-keyboard] .knobs-cmp-handle:focus::after{box-shadow:0 1px 3px rgba(0,0,0,.35),0 0 0 3px color-mix(in srgb,var(--_k-accent) 40%,transparent)}\n/* The split's hairline, down the middle side by side. */\n.knobs-cmp-split{position:absolute;left:0;top:0;width:1px;height:0;margin-left:-.5px;background:color-mix(in srgb,var(--_k-bg) 72%,transparent);pointer-events:none;will-change:transform,opacity}\n/* Zoomed: how far, and a way back, in the stage's corner; it fades in and out, nothing shifts. */\n.knobs-cmp-zoom{position:absolute;right:8px;bottom:8px;display:flex;align-items:center;gap:4px;height:26px;padding:0 3px 0 10px;border-radius:999px;\n color:var(--_k-fg);font-size:var(--_k-text-meta);font-variant-numeric:tabular-nums;font-feature-settings:\"tnum\";white-space:nowrap;\n opacity:0;scale:.96;pointer-events:none;transition:opacity .16s cubic-bezier(.4,0,1,1),scale .16s cubic-bezier(.4,0,1,1)}\n.knobs-cmp-zoom[data-on]{opacity:1;scale:1;pointer-events:auto;transition:opacity .2s cubic-bezier(.2,.8,.2,1),scale .2s cubic-bezier(.2,.8,.2,1)}\n:where(.knobs-ui) .knobs-cmp-zoom-reset{all:revert;box-sizing:border-box;font:inherit;letter-spacing:inherit;margin:0;border:0;outline:none;box-shadow:none;\n height:20px;padding:0 8px;border-radius:999px;background:transparent;color:var(--_k-fg-2);cursor:pointer;transition:color .15s,background .15s}\n:where(.knobs-ui) .knobs-cmp-zoom-reset:is(:hover,:focus-visible){color:var(--_k-fg);background:color-mix(in srgb,var(--_k-fg) 12%,transparent)}\n.knobs-cmp-note{position:absolute;left:50%;top:50%;translate:-50% -50%;color:var(--_k-fg-3);font-size:var(--_k-text-meta);pointer-events:none}\n.knobs-cmp-note:empty{display:none}\n.knobs-cmp-strip-host{flex:none;height:60px;min-height:0}\n.knobs-cmp-timeline-host{flex:none;height:56px;min-height:0}\n/* The frame strip: Before over After per column, a difference bar under them, a dot where it peaks. */\n.knobs-ui.knobs-cmp-strip{--_ku-cmp-diff:var(--k-compare-diff,var(--_k-ok));position:relative;width:100%;height:100%;min-height:40px;overflow:hidden;cursor:col-resize;touch-action:none;\n font:var(--_k-weight) var(--_k-font-size)/1.35 var(--_k-font);color:var(--_k-fg)}\n.knobs-cmp-strip>canvas{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}\n@media(prefers-reduced-motion:reduce){:is(.knobs-compare,.knobs-cmp-strip) :where(*,::before,::after){transition-duration:.12s!important}}\n";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface CachedFrame {
|
|
2
|
+
readonly frame: number;
|
|
3
|
+
readonly bitmap: ImageBitmap;
|
|
4
|
+
/** Full size (else a small grid frame). */
|
|
5
|
+
readonly full: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface FramePoint {
|
|
8
|
+
readonly frame: number;
|
|
9
|
+
/** Any cached frame this close (frames) will do. */
|
|
10
|
+
readonly tolerance: number;
|
|
11
|
+
}
|
|
12
|
+
export interface FrameCache {
|
|
13
|
+
readonly src: string;
|
|
14
|
+
/** How it decodes, once it knows. */
|
|
15
|
+
readonly backend: "webcodecs" | "video" | "pending";
|
|
16
|
+
/** Frames held now. */
|
|
17
|
+
readonly size: number;
|
|
18
|
+
/** The cached frame nearest `frame`, no further than `within` frames. */
|
|
19
|
+
nearest(frame: number, within?: number): CachedFrame | null;
|
|
20
|
+
/** The playhead of the viewer being used: full-size frames around it decode first. */
|
|
21
|
+
focus(frame: number): void;
|
|
22
|
+
/** Frames a strip shows (small, with a tolerance each). */
|
|
23
|
+
want(points: readonly FramePoint[]): void;
|
|
24
|
+
/** A frame the viewer has decoded exactly (paused on it). */
|
|
25
|
+
keep(frame: number, source: CanvasImageSource, width: number, height: number): void;
|
|
26
|
+
/** Whether a viewer of it is near the screen: only then does it decode. */
|
|
27
|
+
visible(on: boolean): void;
|
|
28
|
+
/** New frames arrived. */
|
|
29
|
+
listen(f: () => void): () => void;
|
|
30
|
+
release(): void;
|
|
31
|
+
}
|
|
32
|
+
/** Frames either side of the focus decoded at full size. */
|
|
33
|
+
export declare const FOCUS_RADIUS = 6;
|
|
34
|
+
/** The grid's frames: `count` evenly across `total`, as points with half a step's tolerance. */
|
|
35
|
+
export declare function gridPoints(total: number, count?: number): FramePoint[];
|
|
36
|
+
/** The full-size frames wanted around `focus`, nearest first. */
|
|
37
|
+
export declare function focusWindow(focus: number, total: number, radius?: number): number[];
|
|
38
|
+
/** The key in `keys` nearest `frame` within `within` (ties go to the later frame). */
|
|
39
|
+
export declare function nearestKey(keys: Iterable<number>, frame: number, within?: number): number | null;
|
|
40
|
+
/** The shared cache for `src` at `fps`; `release()` it when done. */
|
|
41
|
+
export declare function acquireFrames(doc: Document, src: string, fps: number): FrameCache;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Rect } from "./compare-model.js";
|
|
2
|
+
export declare const MODE_INDEX: {
|
|
3
|
+
readonly side: 0;
|
|
4
|
+
readonly wipe: 1;
|
|
5
|
+
readonly difference: 2;
|
|
6
|
+
readonly flicker: 3;
|
|
7
|
+
};
|
|
8
|
+
export interface CompareDraw {
|
|
9
|
+
/** Device px: where each side's picture lands (zoomed, it's larger than its clip). */
|
|
10
|
+
readonly before: Rect;
|
|
11
|
+
readonly after: Rect;
|
|
12
|
+
/** Device px: the part of the stage each side may draw in. */
|
|
13
|
+
readonly beforeClip: Rect;
|
|
14
|
+
readonly afterClip: Rect;
|
|
15
|
+
readonly width: number;
|
|
16
|
+
readonly height: number;
|
|
17
|
+
readonly dpr: number;
|
|
18
|
+
/** The wipe's line (device px) and its soft edge (device px). */
|
|
19
|
+
readonly wipeX: number;
|
|
20
|
+
readonly soft: number;
|
|
21
|
+
readonly from: number;
|
|
22
|
+
readonly to: number;
|
|
23
|
+
/** 0 shows `from`, 1 shows `to`. */
|
|
24
|
+
readonly mix: number;
|
|
25
|
+
/** Before in After's place (X held). */
|
|
26
|
+
readonly flick: boolean;
|
|
27
|
+
/** Flicker mode's phase: Before showing. */
|
|
28
|
+
readonly phase: boolean;
|
|
29
|
+
readonly threshold: number;
|
|
30
|
+
readonly gain: number;
|
|
31
|
+
readonly accent: readonly [number, number, number];
|
|
32
|
+
/** Device px centre and radius, and zoom; null for none. */
|
|
33
|
+
readonly loupe: {
|
|
34
|
+
readonly x: number;
|
|
35
|
+
readonly y: number;
|
|
36
|
+
readonly r: number;
|
|
37
|
+
readonly zoom: number;
|
|
38
|
+
} | null;
|
|
39
|
+
/** Each side shows its preview texture (a cached frame) instead of its video's. */
|
|
40
|
+
readonly preview: readonly [boolean, boolean];
|
|
41
|
+
}
|
|
42
|
+
export interface CompareGpu {
|
|
43
|
+
/**
|
|
44
|
+
* Copies a frame in (0 Before, 1 After; 2 and 3 their previews); false if it couldn't (no data
|
|
45
|
+
* yet, or a tainted source).
|
|
46
|
+
*/
|
|
47
|
+
upload(slot: 0 | 1 | 2 | 3, source: CanvasImageSource, w: number, h: number): boolean;
|
|
48
|
+
/** Forgets a side's frame (a new source is loading). */
|
|
49
|
+
clear(side: 0 | 1): void;
|
|
50
|
+
/** Frees the textures and shrinks the canvas (off screen); uploads bring them back. */
|
|
51
|
+
shrink(): void;
|
|
52
|
+
draw(frame: CompareDraw): void;
|
|
53
|
+
dispose(): void;
|
|
54
|
+
}
|
|
55
|
+
/** A renderer on `canvas`, or null where WebGPU isn't there. `onLost` hears a lost device. */
|
|
56
|
+
export declare function createCompareGpu(canvas: HTMLCanvasElement, onLost: () => void): Promise<CompareGpu | null>;
|
|
57
|
+
/** The shared device once made (null before it is, or without WebGPU): for checks. */
|
|
58
|
+
export declare const sharedDevice: () => Promise<unknown>;
|
|
@@ -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;
|