@kitlangton/knobs 0.4.0-next.16 → 0.4.0-next.17
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 +28 -9
- package/dist/dev/{chunk-v2g91mrm.js → chunk-2g0znytq.js} +1 -0
- package/dist/dev/{chunk-9y93net8.js → chunk-3vag4n3q.js} +39 -9
- package/dist/dev/{chunk-e5hbs8fe.js → chunk-5cv3g7ca.js} +205 -64
- package/dist/dev/{chunk-exaya50m.js → chunk-xzm0zawh.js} +6 -6
- package/dist/dev/index.js +9 -8
- package/dist/index.js +7 -5
- package/dist/panel.js +292 -83
- package/dist/review-react.js +1341 -880
- package/dist/review-solid.js +1341 -880
- package/dist/review.js +1711 -1248
- package/dist/themes.js +8 -0
- package/dist/types/core/schema.d.ts +7 -1
- package/dist/types/panel/description-ids.d.ts +6 -0
- package/dist/types/panel/folds.d.ts +7 -4
- package/dist/types/panel/gpu/color.d.ts +2 -0
- package/dist/types/panel/page-hints.d.ts +3 -2
- package/dist/types/panel/position.d.ts +23 -0
- package/dist/types/tokens/schema.d.ts +9 -0
- package/dist/types/ui/compare-css.d.ts +1 -1
- package/dist/types/ui/compare-frames.d.ts +64 -2
- package/dist/types/ui/compare-model.d.ts +23 -0
- package/dist/types/ui/compare.d.ts +6 -4
- package/dist/types/ui/player-css.d.ts +1 -1
- package/dist/types/ui/player.d.ts +6 -0
- package/dist/types/ui/react.d.ts +1 -1
- package/dist/types/ui/solid.d.ts +1 -1
- package/dist/types/ui/tooltip-model.d.ts +35 -0
- package/dist/types/ui/tooltip.d.ts +1 -1
- package/dist/types/ui/walkthrough-model.d.ts +4 -2
- package/dist/ui-react.js +2015 -1507
- package/dist/ui-solid.js +2014 -1506
- package/dist/ui.js +2013 -1505
- package/package.json +1 -1
package/dist/themes.js
CHANGED
|
@@ -520,6 +520,14 @@ var TOKENS = [
|
|
|
520
520
|
doc: "Panel and card radius, concentric with controls.",
|
|
521
521
|
deprecated: ["--k-panel-radius", "--ku-panel-radius"]
|
|
522
522
|
},
|
|
523
|
+
{
|
|
524
|
+
name: "radius-frame",
|
|
525
|
+
tier: "semantic",
|
|
526
|
+
type: "length",
|
|
527
|
+
group: "Shape",
|
|
528
|
+
source: css("calc(var(--k-radius) + min(var(--k-radius) * 999, var(--_k-pad) + 1px))"),
|
|
529
|
+
doc: "A tuning panel's outer corners: its cards' radius plus its padding and border, so the cards, the preset strip and the focus lens inside run concentric. Squares when --k-radius is 0."
|
|
530
|
+
},
|
|
523
531
|
{
|
|
524
532
|
name: "round",
|
|
525
533
|
tier: "semantic",
|
|
@@ -5,6 +5,11 @@ import { CURVE_SHAPES, type CurveInterp, type CurvePoint, type GradientSpace, ty
|
|
|
5
5
|
import type { SpringValue } from "./spring.js";
|
|
6
6
|
export interface FieldMeta {
|
|
7
7
|
readonly label?: string;
|
|
8
|
+
/**
|
|
9
|
+
* What the knob does, in a sentence: a tooltip over its row (after a pause, or on keyboard
|
|
10
|
+
* focus) and its control's accessible description. Search matches it.
|
|
11
|
+
*/
|
|
12
|
+
readonly description?: string;
|
|
8
13
|
readonly group?: string;
|
|
9
14
|
/**
|
|
10
15
|
* What the group's header shows when it isn't the `group` id itself (the first field that
|
|
@@ -71,7 +76,7 @@ export interface FieldMeta {
|
|
|
71
76
|
readonly subsection?: string;
|
|
72
77
|
}
|
|
73
78
|
/** Meta keys copied from a field's options or a descriptor onto the field. */
|
|
74
|
-
export declare const META_KEYS: readonly ["label", "group", "groupLabel", "section", "sectionLabel", "groupDescription", "sectionDescription", "shortcut", "random", "disabled", "readOnly", "folded", "sectionFolded", "tab", "pinned", "inPresets", "subsection"];
|
|
79
|
+
export declare const META_KEYS: readonly ["label", "description", "group", "groupLabel", "section", "sectionLabel", "groupDescription", "sectionDescription", "shortcut", "random", "disabled", "readOnly", "folded", "sectionFolded", "tab", "pinned", "inPresets", "subsection"];
|
|
75
80
|
/** The meta `source` gives, without undefined keys. */
|
|
76
81
|
export declare function pickMeta(source: FieldMeta | undefined, skip?: readonly string[]): FieldMeta;
|
|
77
82
|
/** Marks a knob as half of a flat spring: two number keys drawn as one spring card. */
|
|
@@ -493,6 +498,7 @@ export declare function range(value: readonly [number, number], options: FieldMe
|
|
|
493
498
|
unit?: string;
|
|
494
499
|
decimals?: number;
|
|
495
500
|
label?: string;
|
|
501
|
+
description?: string;
|
|
496
502
|
group?: string;
|
|
497
503
|
groupLabel?: string;
|
|
498
504
|
section?: string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ids for one panel's row descriptions (`aria-describedby`), unique in the page: each panel gets
|
|
3
|
+
* its own number, so a store mounted twice doesn't repeat them, and the store's id is cut to the
|
|
4
|
+
* characters an id reference list can carry.
|
|
5
|
+
*/
|
|
6
|
+
export declare function descriptionIds(storeId: string): () => string;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { type StoragePort } from "../core/storage.js";
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* What a page or panel remembers folded or open: a page's sections, group cards, and a panel's
|
|
4
|
+
* sub-headings.
|
|
5
|
+
*/
|
|
6
|
+
export type FoldKind = "sections" | "groups" | "subsections";
|
|
4
7
|
export interface FoldMemory {
|
|
5
8
|
/** What the user last chose for it, if anything. */
|
|
6
9
|
get(kind: FoldKind, id: string): boolean | undefined;
|
|
7
10
|
set(kind: FoldKind, id: string, folded: boolean): void;
|
|
8
11
|
}
|
|
9
12
|
/**
|
|
10
|
-
* A page's folds, kept under `knobs:<id>:folds:v1` in `storage` as
|
|
11
|
-
* `{ sections
|
|
13
|
+
* A page's or panel's folds, kept under `knobs:<id>:folds:v1` in `storage` as
|
|
14
|
+
* `{ sections, groups, subsections? }` (each a `Record<string, boolean>`), and nothing else (no
|
|
12
15
|
* values, locks or variants). `null` storage remembers nothing.
|
|
13
16
|
*/
|
|
14
17
|
export declare function foldMemory(id: string, storage: () => StoragePort | null): FoldMemory;
|
|
@@ -70,6 +70,8 @@ export interface PanelThemeTokens {
|
|
|
70
70
|
readonly trayPad: number;
|
|
71
71
|
readonly radius: number;
|
|
72
72
|
readonly panelRadius: number;
|
|
73
|
+
/** A tuning panel's own outer corners, concentric with its cards (`--k-radius-frame`). */
|
|
74
|
+
readonly frameRadius: number;
|
|
73
75
|
readonly radiusMd: number;
|
|
74
76
|
readonly radiusSm: number;
|
|
75
77
|
readonly fontSize: number;
|
|
@@ -56,8 +56,9 @@ export interface PageHints {
|
|
|
56
56
|
stop(): void;
|
|
57
57
|
readonly active: boolean;
|
|
58
58
|
/**
|
|
59
|
-
* Whether `s`
|
|
60
|
-
* something else on the page to label too.
|
|
59
|
+
* Whether `s` in a knobs/ui piece starts hint mode: a panel or host has registered, and there's
|
|
60
|
+
* something else on the page to label too. On the page itself it does only when the host asked
|
|
61
|
+
* (targets of its own, or `[data-hint]` elements).
|
|
61
62
|
*/
|
|
62
63
|
readonly listening: boolean;
|
|
63
64
|
}
|
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
import { type StoragePort } from "../core/storage.js";
|
|
2
|
+
/** Where a floating panel was left: `x` from the left edge, or from the right with `right`. */
|
|
3
|
+
export interface SavedPosition {
|
|
4
|
+
readonly x: number;
|
|
5
|
+
readonly y: number;
|
|
6
|
+
readonly right?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The position to keep for a panel dropped at `left`, `top`: a panel on the right half keeps its
|
|
10
|
+
* distance from the right edge, so a wider or narrower window leaves it by the same edge.
|
|
11
|
+
*/
|
|
12
|
+
export declare function savedPosition(left: number, top: number, width: number, view: number): {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
right: true;
|
|
16
|
+
} | {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
right?: undefined;
|
|
20
|
+
};
|
|
21
|
+
/** The left edge a saved position asks for in a `view` px wide window. */
|
|
22
|
+
export declare const leftOf: (saved: SavedPosition, width: number, view: number) => number;
|
|
23
|
+
/** Reads a saved position, or `undefined` for none (or one that isn't a position). */
|
|
24
|
+
export declare function readPosition(raw: string | null | undefined): SavedPosition | undefined;
|
|
2
25
|
export declare function positionPanel(panel: HTMLElement, header: HTMLElement, anchor: HTMLElement, id: string, storage: () => StoragePort | null): {
|
|
3
26
|
constrain: () => void;
|
|
4
27
|
dispose(): void;
|
|
@@ -491,6 +491,15 @@ export declare const TOKENS: readonly [{
|
|
|
491
491
|
};
|
|
492
492
|
readonly doc: "Panel and card radius, concentric with controls.";
|
|
493
493
|
readonly deprecated: readonly ["--k-panel-radius", "--ku-panel-radius"];
|
|
494
|
+
}, {
|
|
495
|
+
readonly name: "radius-frame";
|
|
496
|
+
readonly tier: "semantic";
|
|
497
|
+
readonly type: "length";
|
|
498
|
+
readonly group: "Shape";
|
|
499
|
+
readonly source: {
|
|
500
|
+
readonly css: "calc(var(--k-radius) + min(var(--k-radius) * 999, var(--_k-pad) + 1px))";
|
|
501
|
+
};
|
|
502
|
+
readonly doc: "A tuning panel's outer corners: its cards' radius plus its padding and border, so the cards, the preset strip and the focus lens inside run concentric. Squares when --k-radius is 0.";
|
|
494
503
|
}, {
|
|
495
504
|
readonly name: "round";
|
|
496
505
|
readonly tier: "semantic";
|
|
@@ -1 +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-type-small)/var(--_k-leading-small) 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:var(--_k-gap);height:var(--_k-control-h)}\n/* The modes: one fill, the selection pill on the one picked; a mode isn't brighter for being\n picked: the pill's bright copy is. */\n.knobs-cmp-modes{position:relative;display:inline-flex;height:var(--_k-control-h);border-radius:var(--_k-radius);background:var(--_k-fill)}\n.knobs-cmp-modes[hidden]{display:none}\n/* Hover is a quiet fill under the arrow cursor (no hand, no brightening), there at once. */\n.knobs-ui .knobs-cmp-mode{display:inline-flex;align-items:center;height:100%;padding:0 8px;border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:default;white-space:nowrap;\n transition:background calc(var(--_k-hover-out) * 1ms)}\n.knobs-ui .knobs-cmp-mode:not([aria-checked=\"true\"]):is(:hover,:focus-visible){background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}\n/* Toggles: icon buttons, the accent's wash when on. */\n.knobs-ui .knobs-cmp-toggle{display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:default;\n transition:background calc(var(--_k-hover-out) * 1ms)}\n.knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}\n.knobs-ui .knobs-cmp-toggle[aria-pressed=\"true\"]{color:var(--_k-accent-fg);background:var(--_k-accent-soft)}\n.knobs-cmp-toggle svg{display:block}\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/* Zoomed, it pans: a move cursor, never a hand. */\n.knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:move;touch-action:none}\n.knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:move!important}\n/* Wiping, the grip stands in for the cursor. */\n.knobs-compare[data-dragging],.knobs-compare[data-dragging] *{cursor:none!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/* The media comes in with its first frames: a fade and a 4px blur on the default spring. */\n.knobs-cmp-canvas,.knobs-compare[data-renderer=\"dom\"] .knobs-cmp-media{transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}\n.knobs-compare:not([data-ready]) :is(.knobs-cmp-canvas,.knobs-cmp-media){opacity:0;filter:blur(4px)}\n/* Over the pictures: a quiet scrim, dark in either appearance, blurred, with no ring. */\n.knobs-cmp-label,.knobs-cmp-zoom,.knobs-cmp-grip{background:var(--_ku-media-scrim);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}\n/* \u2717 Before \u00B7 \u2713 After: Before quieter, After in full. Neutral: no colour of its own. */\n.knobs-cmp-label{position:absolute;left:0;top:0;display:flex;align-items:center;gap:4px;padding:1px 7px 1px 5px;border-radius:999px;font-weight:var(--_k-weight-label);\n white-space:nowrap;pointer-events:none;will-change:transform}\n.knobs-cmp-label[data-side=\"before\"]{color:var(--_ku-on-media-2)}\n.knobs-cmp-label[data-side=\"after\"]{color:var(--_ku-on-media)}\n/* Fusing, the mark folds away and the label is its name. */\n.knobs-cmp-mark{flex:none;display:flex;justify-content:flex-end;overflow:hidden}\n.knobs-cmp-mark svg{display:block;flex:none;width:12px;height:12px}\n/* The fuse's seam: a blurred strip over where the sides meet, soft at both sides. */\n.knobs-cmp-seam{position:absolute;left:0;top:0;width:8px;height:0;opacity:0;pointer-events:none;will-change:transform,opacity;\n -webkit-mask-image:linear-gradient(to right,transparent,#000 35%,#000 65%,transparent);mask-image:linear-gradient(to right,transparent,#000 35%,#000 65%,transparent)}\n/* The wipe: a hairline down the line with a raised grip in its middle (at the pointer's height\n while dragged); a wider strip takes the hand and the keys (a slider). It fades in with the\n fuse; wiping, line and grip brighten. */\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,opacity}\n.knobs-cmp-handle[data-on]{pointer-events:auto}\n.knobs-cmp-handle::before{content:\"\";position:absolute;top:0;bottom:0;left:11.5px;width:1px;background:color-mix(in srgb,var(--_ku-cmp-line) 72%,transparent);box-shadow:0 0 6px var(--_ku-media-scrim)}\n.knobs-compare[data-dragging] .knobs-cmp-handle::before{background:var(--_ku-cmp-line)}\n.knobs-cmp-grip{position:absolute;top:0;left:4px;width:16px;height:28px;border-radius:min(8px,calc(var(--_k-radius) * 999));will-change:transform}\n.knobs-cmp-grip::before,.knobs-cmp-grip::after{content:\"\";position:absolute;top:10px;width:1px;height:8px;background:var(--_ku-on-media-2);transition:background calc(var(--_k-hover-out) * 1ms)}\n.knobs-cmp-grip::before{left:6px}\n.knobs-cmp-grip::after{left:9px}\n.knobs-compare[data-dragging] .knobs-cmp-grip::before,.knobs-compare[data-dragging] .knobs-cmp-grip::after{background:var(--_ku-on-media)}\n.knobs-cmp-handle:focus-visible .knobs-cmp-grip,.knobs-compare[data-keyboard] .knobs-cmp-handle:focus .knobs-cmp-grip{box-shadow:0 0 0 2px var(--_k-accent)}\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(--_ku-on-media);font-variant-numeric:tabular-nums;font-feature-settings:var(--_kr-data,\"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 display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border-radius:999px;background:transparent;color:var(--_ku-on-media-2);cursor:default;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}\n:where(.knobs-ui) .knobs-cmp-zoom-reset:is(:hover,:focus-visible){background:color-mix(in srgb,var(--_ku-on-media) 16%,transparent);color:var(--_ku-on-media);transition-duration:calc(var(--_k-hover-in) * 1ms)}\n.knobs-cmp-note{position:absolute;left:50%;top:50%;translate:-50% -50%;color:var(--_k-fg-3);pointer-events:none}\n.knobs-cmp-note:empty{display:none}\n.knobs-cmp-
|
|
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-type-small)/var(--_k-leading-small) 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:var(--_k-gap);height:var(--_k-control-h)}\n/* The modes: one fill, the selection pill on the one picked; a mode isn't brighter for being\n picked: the pill's bright copy is. */\n.knobs-cmp-modes{position:relative;display:inline-flex;height:var(--_k-control-h);border-radius:var(--_k-radius);background:var(--_k-fill)}\n.knobs-cmp-modes[hidden]{display:none}\n/* Hover is a quiet fill under the arrow cursor (no hand, no brightening), there at once. */\n.knobs-ui .knobs-cmp-mode{display:inline-flex;align-items:center;height:100%;padding:0 8px;border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:default;white-space:nowrap;\n transition:background calc(var(--_k-hover-out) * 1ms)}\n.knobs-ui .knobs-cmp-mode:not([aria-checked=\"true\"]):is(:hover,:focus-visible){background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}\n/* Toggles: icon buttons, the accent's wash when on. */\n.knobs-ui .knobs-cmp-toggle{display:inline-flex;align-items:center;justify-content:center;width:var(--_k-control-h);height:var(--_k-control-h);border-radius:var(--_k-radius);color:var(--_k-fg-2);cursor:default;\n transition:background calc(var(--_k-hover-out) * 1ms)}\n.knobs-ui .knobs-cmp-toggle:is(:hover,:focus-visible){background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}\n.knobs-ui .knobs-cmp-toggle[aria-pressed=\"true\"]{color:var(--_k-accent-fg);background:var(--_k-accent-soft)}\n.knobs-cmp-toggle svg{display:block}\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/* Zoomed, it pans: a move cursor, never a hand. */\n.knobs-compare[data-zoomed] .knobs-cmp-stage{cursor:move;touch-action:none}\n.knobs-compare[data-panning],.knobs-compare[data-panning] *{cursor:move!important}\n/* Wiping, the grip stands in for the cursor. */\n.knobs-compare[data-dragging],.knobs-compare[data-dragging] *{cursor:none!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/* The media comes in with its first frames: a fade and a 4px blur on the default spring. */\n.knobs-cmp-canvas,.knobs-compare[data-renderer=\"dom\"] .knobs-cmp-media{transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}\n.knobs-compare:not([data-ready]) :is(.knobs-cmp-canvas,.knobs-cmp-media){opacity:0;filter:blur(4px)}\n/* Over the pictures: a quiet scrim, dark in either appearance, blurred, with no ring. */\n.knobs-cmp-label,.knobs-cmp-zoom,.knobs-cmp-grip{background:var(--_ku-media-scrim);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}\n/* \u2717 Before \u00B7 \u2713 After: Before quieter, After in full. Neutral: no colour of its own. */\n.knobs-cmp-label{position:absolute;left:0;top:0;display:flex;align-items:center;gap:4px;padding:1px 7px 1px 5px;border-radius:999px;font-weight:var(--_k-weight-label);\n white-space:nowrap;pointer-events:none;will-change:transform}\n.knobs-cmp-label[data-side=\"before\"]{color:var(--_ku-on-media-2)}\n.knobs-cmp-label[data-side=\"after\"]{color:var(--_ku-on-media)}\n/* Fusing, the mark folds away and the label is its name. */\n.knobs-cmp-mark{flex:none;display:flex;justify-content:flex-end;overflow:hidden}\n.knobs-cmp-mark svg{display:block;flex:none;width:12px;height:12px}\n/* The fuse's seam: a blurred strip over where the sides meet, soft at both sides. */\n.knobs-cmp-seam{position:absolute;left:0;top:0;width:8px;height:0;opacity:0;pointer-events:none;will-change:transform,opacity;\n -webkit-mask-image:linear-gradient(to right,transparent,#000 35%,#000 65%,transparent);mask-image:linear-gradient(to right,transparent,#000 35%,#000 65%,transparent)}\n/* The wipe: a hairline down the line with a raised grip in its middle (at the pointer's height\n while dragged); a wider strip takes the hand and the keys (a slider). It fades in with the\n fuse; wiping, line and grip brighten. */\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,opacity}\n.knobs-cmp-handle[data-on]{pointer-events:auto}\n.knobs-cmp-handle::before{content:\"\";position:absolute;top:0;bottom:0;left:11.5px;width:1px;background:color-mix(in srgb,var(--_ku-cmp-line) 72%,transparent);box-shadow:0 0 6px var(--_ku-media-scrim)}\n.knobs-compare[data-dragging] .knobs-cmp-handle::before{background:var(--_ku-cmp-line)}\n.knobs-cmp-grip{position:absolute;top:0;left:4px;width:16px;height:28px;border-radius:min(8px,calc(var(--_k-radius) * 999));will-change:transform}\n.knobs-cmp-grip::before,.knobs-cmp-grip::after{content:\"\";position:absolute;top:10px;width:1px;height:8px;background:var(--_ku-on-media-2);transition:background calc(var(--_k-hover-out) * 1ms)}\n.knobs-cmp-grip::before{left:6px}\n.knobs-cmp-grip::after{left:9px}\n.knobs-compare[data-dragging] .knobs-cmp-grip::before,.knobs-compare[data-dragging] .knobs-cmp-grip::after{background:var(--_ku-on-media)}\n.knobs-cmp-handle:focus-visible .knobs-cmp-grip,.knobs-compare[data-keyboard] .knobs-cmp-handle:focus .knobs-cmp-grip{box-shadow:0 0 0 2px var(--_k-accent)}\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(--_ku-on-media);font-variant-numeric:tabular-nums;font-feature-settings:var(--_kr-data,\"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 display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border-radius:999px;background:transparent;color:var(--_ku-on-media-2);cursor:default;transition:background calc(var(--_k-hover-out) * 1ms),color calc(var(--_k-hover-out) * 1ms)}\n:where(.knobs-ui) .knobs-cmp-zoom-reset:is(:hover,:focus-visible){background:color-mix(in srgb,var(--_ku-on-media) 16%,transparent);color:var(--_ku-on-media);transition-duration:calc(var(--_k-hover-in) * 1ms)}\n.knobs-cmp-note{position:absolute;left:50%;top:50%;translate:-50% -50%;color:var(--_k-fg-3);pointer-events:none}\n.knobs-cmp-note:empty{display:none}\n.knobs-cmp-timeline-host{flex:none;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-type-small)/var(--_k-leading-small) 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";
|
|
@@ -15,10 +15,23 @@ export interface FrameCache {
|
|
|
15
15
|
readonly backend: "webcodecs" | "video" | "pending";
|
|
16
16
|
/** Frames held now. */
|
|
17
17
|
readonly size: number;
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/** Scrub proxies held now, and every how many frames they are. */
|
|
19
|
+
readonly proxies: {
|
|
20
|
+
readonly count: number;
|
|
21
|
+
readonly stride: number;
|
|
22
|
+
readonly bytes: number;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The cached frame nearest `frame`, no further than `within` frames. `decode`: a nearer scrub
|
|
26
|
+
* proxy that's stored but not decoded starts decoding (listeners hear when it's in).
|
|
27
|
+
*/
|
|
28
|
+
nearest(frame: number, within?: number, decode?: boolean): CachedFrame | null;
|
|
29
|
+
/** Whether a frame this close to `frame` is held, decoded or as a proxy (within its stride). */
|
|
30
|
+
covers(frame: number): boolean;
|
|
20
31
|
/** The playhead of the viewer being used: full-size frames around it decode first. */
|
|
21
32
|
focus(frame: number): void;
|
|
33
|
+
/** The playhead of a viewer being scrubbed: this cache goes first, nothing full-size is asked. */
|
|
34
|
+
follow(frame: number): void;
|
|
22
35
|
/** Frames a strip shows (small, with a tolerance each). */
|
|
23
36
|
want(points: readonly FramePoint[]): void;
|
|
24
37
|
/** A frame the viewer has decoded exactly (paused on it). */
|
|
@@ -31,11 +44,60 @@ export interface FrameCache {
|
|
|
31
44
|
}
|
|
32
45
|
/** Frames either side of the focus decoded at full size. */
|
|
33
46
|
export declare const FOCUS_RADIUS = 6;
|
|
47
|
+
/** A scrub proxy's longest side, JPEG quality, the bytes a source's proxies may hold, how many stay decoded. */
|
|
48
|
+
export declare const PROXY: {
|
|
49
|
+
readonly size: 960;
|
|
50
|
+
readonly quality: 0.8;
|
|
51
|
+
readonly budget: number;
|
|
52
|
+
readonly decoded: 8;
|
|
53
|
+
/** Proxies a unit makes before another cache takes its turn. */
|
|
54
|
+
readonly turn: 90;
|
|
55
|
+
};
|
|
56
|
+
/** What `scrubCaching` reads of a window. */
|
|
57
|
+
export interface DataSaver {
|
|
58
|
+
readonly matchMedia?: (query: string) => {
|
|
59
|
+
readonly matches: boolean;
|
|
60
|
+
};
|
|
61
|
+
readonly navigator?: object;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Whether a viewer caches its frames in the background (it fetches the whole file and decodes
|
|
65
|
+
* it): unless its `scrubCache` option is false, the reader prefers reduced data, or the
|
|
66
|
+
* connection is saving data. Without it, scrubbing seeks the video as it goes.
|
|
67
|
+
*/
|
|
68
|
+
export declare function scrubCaching(option: boolean | undefined, win: DataSaver | null | undefined): boolean;
|
|
69
|
+
/** The stride a set of proxies needs to stay in `budget`: the smallest power of two that does. */
|
|
70
|
+
export declare function proxyStride(bytes: number, count: number, total: number, budget: number): number;
|
|
71
|
+
/** Where a hand moving from `last` to `frame` is likely to be next (within 0 and `total`). */
|
|
72
|
+
export declare const aheadOf: (frame: number, last: number | null, total: number) => number | null;
|
|
34
73
|
/** The grid's frames: `count` evenly across `total`, as points with half a step's tolerance. */
|
|
35
74
|
export declare function gridPoints(total: number, count?: number): FramePoint[];
|
|
36
75
|
/** The full-size frames wanted around `focus`, nearest first. */
|
|
37
76
|
export declare function focusWindow(focus: number, total: number, radius?: number): number[];
|
|
38
77
|
/** The key in `keys` nearest `frame` within `within` (ties go to the later frame). */
|
|
39
78
|
export declare function nearestKey(keys: Iterable<number>, frame: number, within?: number): number | null;
|
|
79
|
+
/** What the encoder pool needs of a window (a stand-in in tests). */
|
|
80
|
+
export interface EncoderHost {
|
|
81
|
+
readonly Worker?: new (url: string) => Worker;
|
|
82
|
+
readonly OffscreenCanvas?: unknown;
|
|
83
|
+
readonly URL: Pick<typeof URL, "createObjectURL" | "revokeObjectURL">;
|
|
84
|
+
readonly Blob: typeof Blob;
|
|
85
|
+
}
|
|
86
|
+
export interface Encoders {
|
|
87
|
+
readonly workers: Worker[];
|
|
88
|
+
readonly waiting: Map<number, {
|
|
89
|
+
done(b: Blob): void;
|
|
90
|
+
fail(e: unknown): void;
|
|
91
|
+
}>;
|
|
92
|
+
next: number;
|
|
93
|
+
}
|
|
94
|
+
/** A cache uses the pool: it's made the first time a proxy is encoded. */
|
|
95
|
+
export declare function holdEncoders(): void;
|
|
96
|
+
/** A cache is done with the pool: the last one ends its workers, and the next starts afresh. */
|
|
97
|
+
export declare function dropEncoders(): void;
|
|
98
|
+
/** The page's encoder pool, made on first use; null where workers can't run. */
|
|
99
|
+
export declare function encodersOf(host: EncoderHost): Encoders | null;
|
|
100
|
+
/** `bitmap` as a JPEG on one of the pool's workers (it takes the bitmap). */
|
|
101
|
+
export declare function encodeOn(pool: Encoders, bitmap: ImageBitmap, quality: number): Promise<Blob>;
|
|
40
102
|
/** The shared cache for `src` at `fps`; `release()` it when done. */
|
|
41
103
|
export declare function acquireFrames(doc: Document, src: string, fps: number): FrameCache;
|
|
@@ -67,6 +67,29 @@ export interface SeekStep {
|
|
|
67
67
|
export declare function requestSeek(s: SeekState, t: number, current: number, fps: number): SeekStep;
|
|
68
68
|
/** The seek in flight landed on `current`: apply what's pending, if it's another frame. */
|
|
69
69
|
export declare function seeked(s: SeekState, current: number, fps: number): SeekStep;
|
|
70
|
+
/**
|
|
71
|
+
* Where a video showing `current` seeks once a scrub lets go at `t`: the midpoint of `t`'s frame,
|
|
72
|
+
* or null when that frame already shows. Frames, not seconds: 1.030 and 1.040 are 1/100 s apart
|
|
73
|
+
* and two frames at 30 fps.
|
|
74
|
+
*/
|
|
75
|
+
export declare function releaseSeek(current: number, t: number, fps: number, duration?: number): number | null;
|
|
76
|
+
/** How long a seek may take to land before the preview over it gives way (ms). */
|
|
77
|
+
export declare const SEEK_STALL_MS = 2500;
|
|
78
|
+
/** What `stallGuard` reads of a window. */
|
|
79
|
+
export interface Timers {
|
|
80
|
+
setTimeout(f: () => void, ms: number): number;
|
|
81
|
+
clearTimeout(id: number): void;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A seek that never lands (a decoder that gave up, a network that went away) mustn't leave a
|
|
85
|
+
* preview over the video forever: `arm` when a seek goes out, `clear` when it lands, and
|
|
86
|
+
* `onStall` runs if it doesn't within `ms`.
|
|
87
|
+
*/
|
|
88
|
+
export declare function stallGuard(timers: Timers, onStall: () => void, ms?: number): {
|
|
89
|
+
arm(): void;
|
|
90
|
+
clear: () => void;
|
|
91
|
+
readonly armed: boolean;
|
|
92
|
+
};
|
|
70
93
|
export interface Rect {
|
|
71
94
|
readonly x: number;
|
|
72
95
|
readonly y: number;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type CompareMode, type MediaKind, type ZoomRegion } from "./compare-model.js";
|
|
2
|
-
import { type CompareStrip } from "./compare-strip.js";
|
|
3
2
|
import { type UiLook } from "./look.js";
|
|
4
3
|
import { type Timeline } from "./timeline.js";
|
|
5
4
|
export type { CompareMode, MediaKind, ZoomRegion } from "./compare-model.js";
|
|
@@ -51,8 +50,6 @@ export interface CompareOptions extends UiLook {
|
|
|
51
50
|
* `play` and keys still work.
|
|
52
51
|
*/
|
|
53
52
|
readonly timeline?: Timeline | boolean;
|
|
54
|
-
/** The frame strip over the timeline (off; one timeline is made if there's none). */
|
|
55
|
-
readonly strip?: boolean;
|
|
56
53
|
/**
|
|
57
54
|
* The sources' width over height before they load (16/9). In a container without a height of
|
|
58
55
|
* its own, the viewer takes the height its sides need: a split screen with no margins.
|
|
@@ -62,6 +59,12 @@ export interface CompareOptions extends UiLook {
|
|
|
62
59
|
readonly lazy?: boolean;
|
|
63
60
|
/** `"gpu"` (the default, where WebGPU is there) or `"dom"`. */
|
|
64
61
|
readonly renderer?: "gpu" | "dom";
|
|
62
|
+
/**
|
|
63
|
+
* Decode the videos' frames in the background, so a step or a scrub shows its frame at once (it
|
|
64
|
+
* fetches each whole file). On unless the reader prefers reduced data or saves data; false
|
|
65
|
+
* never, true always. Without it, the videos seek as the playhead moves.
|
|
66
|
+
*/
|
|
67
|
+
readonly scrubCache?: boolean;
|
|
65
68
|
readonly label?: string;
|
|
66
69
|
/** The modes, the loupe and the wipe take codes in the page's hint mode (`s`); on by default. */
|
|
67
70
|
readonly hints?: boolean;
|
|
@@ -86,7 +89,6 @@ export interface Compare {
|
|
|
86
89
|
readonly renderer: "gpu" | "dom";
|
|
87
90
|
/** The bound timeline, or the one it made. */
|
|
88
91
|
readonly timeline: Timeline | null;
|
|
89
|
-
readonly strip: CompareStrip | null;
|
|
90
92
|
/** The part of each side in view (`{ u: 0, v: 0, w: 1, h: 1 }` is all of it). */
|
|
91
93
|
readonly zoom: ZoomRegion;
|
|
92
94
|
/** The timeline's loop, or null. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const playerCss = "\n:where(.knobs-player,.knobs-player *){box-sizing:border-box}\n.knobs-ui.knobs-player{--_kp-stage:var(--_k-compare-stage);display:flex;flex-direction:column;gap:6px;width:100%;outline:none;\n font:var(--_k-weight) var(--_k-type-small)/var(--_k-leading-small) var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}\n:where(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-ui.knobs-player,.knobs-ui.knobs-player[data-nested]{--_kp-stage:var(--k-compare-stage,transparent)}\n.knobs-player-frame{position:relative;width:100%;aspect-ratio:var(--kp-aspect,16/9);overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_kp-stage)}\n.knobs-player[data-frame=\"flat\"] .knobs-player-frame{border-radius:0}\n.knobs-player-media{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;object-fit:contain;\n opacity:0;filter:blur(4px);transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}\n.knobs-player[data-ready] .knobs-player-media{opacity:1;filter:none}\n.knobs-player-line{flex:none;min-height:0}\n/* The timeline's own stylesheet is look.ts's; what the player's row and every timeline share\n about the hand is set here, after it. No hand cursor anywhere (the arrow, or a resize where it\n resizes); hover is a quiet fill (fill-hover), in at once (hover-in, 0 ms), out easing. */\n.knobs-ui .knobs-tl-play,.knobs-ui.knobs-timeline :is(.knobs-tl-pin,.knobs-tl-rlabel),\n.knobs-ui.knobs-timeline[data-dragging] .knobs-tl-marker :is(.knobs-tl-pin,.knobs-tl-mlabel){cursor:default}\n.knobs-ui .knobs-tl-play:hover,.knobs-tl-edge:hover::after,.knobs-tl-rlabel:hover,.knobs-tl-track:hover .knobs-tl-hover,\n.knobs-timeline[data-variant=\"line\"] .knobs-tl-track:hover .knobs-tl-segs{transition-duration:calc(var(--_k-hover-in) * 1ms)}\n/* The line's row: a minimal play/pause (the filled pair, a 28px target, no fill at rest), then\n the line, centred on it. */\n.knobs-ui.knobs-timeline[data-variant=\"line\"]{gap:4px;align-items:center}\n.knobs-timeline[data-variant=\"line\"] .knobs-tl-transport{display:flex}\n.knobs-ui.knobs-timeline[data-variant=\"line\"] .knobs-tl-play{width:28px;height:28px;min-width:28px;min-height:28px;border-radius:var(--_k-radius);background:transparent;color:var(--_k-fg-2);\n transition:background calc(var(--_k-hover-out) * 1ms),transform .15s var(--_ku-ease)}\n.knobs-ui.knobs-timeline[data-variant=\"line\"][data-playing] .knobs-tl-play{background:transparent;color:var(--_k-fg-2)}\n.knobs-ui.knobs-timeline[data-variant=\"line\"] .knobs-tl-play:hover{background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}\n.knobs-ui.knobs-timeline[data-variant=\"line\"] .knobs-tl-track{cursor:default;transition:background calc(var(--_k-hover-out) * 1ms)}\n.knobs-ui.knobs-timeline[data-variant=\"line\"] .knobs-tl-track:hover{background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}\n";
|
|
1
|
+
export declare const playerCss = "\n:where(.knobs-player,.knobs-player *){box-sizing:border-box}\n.knobs-ui.knobs-player{--_kp-stage:var(--_k-compare-stage);display:flex;flex-direction:column;gap:6px;width:100%;outline:none;\n font:var(--_k-weight) var(--_k-type-small)/var(--_k-leading-small) var(--_k-font);color:var(--_k-fg);letter-spacing:normal;text-align:left}\n:where(.knobs-surface,.knobs-pane:not([data-nested])>.knobs-pane-clip) .knobs-ui.knobs-player,.knobs-ui.knobs-player[data-nested]{--_kp-stage:var(--k-compare-stage,transparent)}\n.knobs-player-frame{position:relative;width:100%;aspect-ratio:var(--kp-aspect,16/9);overflow:hidden;border-radius:var(--_k-radius-panel);background:var(--_kp-stage)}\n.knobs-player[data-frame=\"flat\"] .knobs-player-frame{border-radius:0}\n.knobs-player-media{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;object-fit:contain;\n opacity:0;filter:blur(4px);transition:opacity calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default),filter calc(var(--_k-motion-default-ms) * 1ms) var(--_k-motion-default)}\n.knobs-player[data-ready] .knobs-player-media{opacity:1;filter:none}\n/* Mid-scrub, the cached frame under the hand, over the video until it lands. */\n.knobs-player-preview{position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;pointer-events:none}\n.knobs-player-preview[hidden]{display:none}\n.knobs-player-line{flex:none;min-height:0}\n/* The timeline's own stylesheet is look.ts's; what the player's row and every timeline share\n about the hand is set here, after it. No hand cursor anywhere (the arrow, or a resize where it\n resizes); hover is a quiet fill (fill-hover), in at once (hover-in, 0 ms), out easing. */\n.knobs-ui .knobs-tl-play,.knobs-ui.knobs-timeline :is(.knobs-tl-pin,.knobs-tl-rlabel),\n.knobs-ui.knobs-timeline[data-dragging] .knobs-tl-marker :is(.knobs-tl-pin,.knobs-tl-mlabel){cursor:default}\n.knobs-ui .knobs-tl-play:hover,.knobs-tl-edge:hover::after,.knobs-tl-rlabel:hover,.knobs-tl-track:hover .knobs-tl-hover,\n.knobs-timeline[data-variant=\"line\"] .knobs-tl-track:hover .knobs-tl-segs{transition-duration:calc(var(--_k-hover-in) * 1ms)}\n/* The line's row: a minimal play/pause (the filled pair, a 28px target, no fill at rest), then\n the line, centred on it. */\n.knobs-ui.knobs-timeline[data-variant=\"line\"]{gap:4px;align-items:center}\n.knobs-timeline[data-variant=\"line\"] .knobs-tl-transport{display:flex}\n.knobs-ui.knobs-timeline[data-variant=\"line\"] .knobs-tl-play{width:28px;height:28px;min-width:28px;min-height:28px;border-radius:var(--_k-radius);background:transparent;color:var(--_k-fg-2);\n transition:background calc(var(--_k-hover-out) * 1ms),transform .15s var(--_ku-ease)}\n.knobs-ui.knobs-timeline[data-variant=\"line\"][data-playing] .knobs-tl-play{background:transparent;color:var(--_k-fg-2)}\n.knobs-ui.knobs-timeline[data-variant=\"line\"] .knobs-tl-play:hover{background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}\n.knobs-ui.knobs-timeline[data-variant=\"line\"] .knobs-tl-track{cursor:default;transition:background calc(var(--_k-hover-out) * 1ms)}\n.knobs-ui.knobs-timeline[data-variant=\"line\"] .knobs-tl-track:hover{background:var(--_k-fill-hover);transition-duration:calc(var(--_k-hover-in) * 1ms)}\n";
|
|
@@ -24,6 +24,12 @@ export interface PlayerOptions extends UiLook {
|
|
|
24
24
|
readonly label?: string;
|
|
25
25
|
/** The line's play key and playhead take codes in the page's hint mode (on by default). */
|
|
26
26
|
readonly hints?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Decode its frames in the background once the pointer comes near, so a scrub shows the frame
|
|
29
|
+
* under the hand (it fetches the whole file). On unless the reader prefers reduced data or saves
|
|
30
|
+
* data; false never, true always.
|
|
31
|
+
*/
|
|
32
|
+
readonly scrubCache?: boolean;
|
|
27
33
|
/** Played or paused, at `time` (seconds). */
|
|
28
34
|
onPlay?(playing: boolean, time: number): void;
|
|
29
35
|
/** Sought by the hand (the line, a key, a chapter) or code, at rest. */
|
package/dist/types/ui/react.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export type CompareProps = CompareOptions & {
|
|
|
66
66
|
};
|
|
67
67
|
/**
|
|
68
68
|
* `compare()` as a component. `before`, `after` (by `src`), `mode`, `wipe`, `loupe`, `threshold`
|
|
69
|
-
* and `fps` follow their props; a new `timeline` handle, `
|
|
69
|
+
* and `fps` follow their props; a new `timeline` handle, `renderer` or `scrubCache` makes it afresh;
|
|
70
70
|
* callbacks always call the latest props.
|
|
71
71
|
*/
|
|
72
72
|
export declare function Compare(props: CompareProps): import("react").JSX.Element;
|
package/dist/types/ui/solid.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ export type CompareProps = CompareOptions & {
|
|
|
56
56
|
onReady?: (compare: CompareHandle) => void;
|
|
57
57
|
};
|
|
58
58
|
/**
|
|
59
|
-
* `compare()` as a component, made once (with the `timeline
|
|
59
|
+
* `compare()` as a component, made once (with the `timeline` and `renderer` it starts
|
|
60
60
|
* with). `before`, `after` (by `src`), `mode`, `wipe`, `loupe`, `threshold` and `fps` follow
|
|
61
61
|
* their props; callbacks always call the latest props.
|
|
62
62
|
*/
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Timers and the clock (the window's, or a test's). */
|
|
2
|
+
export interface TipClock {
|
|
3
|
+
set(run: () => void, ms: number): unknown;
|
|
4
|
+
clear(handle: unknown): void;
|
|
5
|
+
now(): number;
|
|
6
|
+
}
|
|
7
|
+
/** What the schedule asks of the page. */
|
|
8
|
+
export interface TipView<T> {
|
|
9
|
+
/** Shows `tip`; `snap` skips the entrance (a hand-off). */
|
|
10
|
+
show(tip: T, snap: boolean): void;
|
|
11
|
+
/** Hides the one showing; `now` skips the exit. */
|
|
12
|
+
hide(now: boolean): void;
|
|
13
|
+
/** Whether `tip`'s target is still in the page when its pause ends. */
|
|
14
|
+
connected(tip: T): boolean;
|
|
15
|
+
}
|
|
16
|
+
/** A neighbour within this long of a tip going shows at once. */
|
|
17
|
+
export declare const HANDOFF_MS = 400;
|
|
18
|
+
export declare const TIP_DELAY_MS = 500;
|
|
19
|
+
export interface TipSchedule<T extends {
|
|
20
|
+
readonly el: object;
|
|
21
|
+
}> {
|
|
22
|
+
readonly shown: T | null;
|
|
23
|
+
readonly pending: T | null;
|
|
24
|
+
/** `tip` wants to show: after its pause, or at once (`now`, or a hand-off). */
|
|
25
|
+
want(tip: T, options?: {
|
|
26
|
+
readonly now?: boolean;
|
|
27
|
+
readonly delay?: number;
|
|
28
|
+
}): void;
|
|
29
|
+
hide(now?: boolean): void;
|
|
30
|
+
/** Focus left: a tip shown or waiting for a target `within` matches goes, or never shows. */
|
|
31
|
+
leave(within: (tip: T) => boolean): void;
|
|
32
|
+
}
|
|
33
|
+
export declare function tipSchedule<T extends {
|
|
34
|
+
readonly el: object;
|
|
35
|
+
}>(clock: TipClock, view: TipView<T>): TipSchedule<T>;
|
|
@@ -13,7 +13,7 @@ export interface TooltipOptions {
|
|
|
13
13
|
export type TooltipContent = string | (() => string) | ({
|
|
14
14
|
readonly text: string | (() => string);
|
|
15
15
|
} & TooltipOptions);
|
|
16
|
-
/** Shows `content` over `el` after a pause
|
|
16
|
+
/** Shows `content` over `el` after a pause, or at once on keyboard focus. Returns the remover. */
|
|
17
17
|
export declare function tooltip(el: HTMLElement, content: TooltipContent, options?: TooltipOptions): () => void;
|
|
18
18
|
/** Turns on `data-tip` (and `data-tip-shortcut`, `data-tip-placement`) tooltips in a document. */
|
|
19
19
|
export declare function tooltips(doc?: Document): void;
|
|
@@ -124,12 +124,12 @@ export type WalkthroughMedia = string | {
|
|
|
124
124
|
readonly kind: "compare";
|
|
125
125
|
readonly before: string | WalkthroughSource;
|
|
126
126
|
readonly after: string | WalkthroughSource;
|
|
127
|
+
/** A small faint line over it, as every kind's `label` is. */
|
|
128
|
+
readonly label?: string;
|
|
127
129
|
/** `"side"` by default (`"wipe"` where the slot is narrow). */
|
|
128
130
|
readonly mode?: CompareMode;
|
|
129
131
|
/** Width over height (`16 / 9`, or `"1100/900"`); read from the video when left out. */
|
|
130
132
|
readonly aspect?: number | string;
|
|
131
|
-
/** The frame strip over the timeline. */
|
|
132
|
-
readonly strip?: boolean;
|
|
133
133
|
/** Pills under it that seek, and markers on its timeline. */
|
|
134
134
|
readonly chapters?: readonly WalkthroughChapter[];
|
|
135
135
|
/** Marks on the pictures, on both sides (and under a zoom). */
|
|
@@ -193,6 +193,7 @@ export type WalkthroughMedia = string | {
|
|
|
193
193
|
readonly kind: "diff";
|
|
194
194
|
/** `git diff` / `git show` output. */
|
|
195
195
|
readonly patch?: string;
|
|
196
|
+
readonly label?: string;
|
|
196
197
|
readonly files?: readonly DiffSource[];
|
|
197
198
|
readonly file?: string;
|
|
198
199
|
readonly layout?: "split" | "unified";
|
|
@@ -239,6 +240,7 @@ export type WalkthroughMedia = string | {
|
|
|
239
240
|
} | {
|
|
240
241
|
readonly kind: "mount";
|
|
241
242
|
readonly mount: (element: HTMLElement) => (() => void) | void;
|
|
243
|
+
readonly label?: string;
|
|
242
244
|
/** Pixels held for it before it mounts. */
|
|
243
245
|
readonly height?: number;
|
|
244
246
|
};
|