@kitlangton/knobs 0.4.0-next.15 → 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 +46 -14
- package/dist/dev/{chunk-v2g91mrm.js → chunk-2g0znytq.js} +1 -0
- package/dist/dev/{chunk-bejz4qcg.js → chunk-3vag4n3q.js} +47 -10
- package/dist/dev/{chunk-49hkdggk.js → chunk-5cv3g7ca.js} +395 -104
- package/dist/dev/{chunk-902v1jgd.js → chunk-xzm0zawh.js} +6 -6
- package/dist/dev/index.js +9 -8
- package/dist/index.js +7 -5
- package/dist/panel.js +549 -121
- 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 +11 -2
- package/dist/types/core/store.d.ts +29 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/panel/active.d.ts +53 -0
- 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/gpu/renderer.d.ts +8 -2
- package/dist/types/panel/index.d.ts +1 -0
- package/dist/types/panel/page-hints.d.ts +3 -2
- package/dist/types/panel/page.d.ts +2 -15
- package/dist/types/panel/position.d.ts +23 -0
- package/dist/types/panel/summary.d.ts +22 -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
|
|
@@ -15,7 +20,10 @@ export interface FieldMeta {
|
|
|
15
20
|
readonly section?: string;
|
|
16
21
|
/** What the section's heading shows (the first field that gives one wins); `section` by default. */
|
|
17
22
|
readonly sectionLabel?: string;
|
|
18
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* One muted line under the group's card title, in a panel or a page (the first field that gives
|
|
25
|
+
* one wins).
|
|
26
|
+
*/
|
|
19
27
|
readonly groupDescription?: string;
|
|
20
28
|
/** One muted line under the section's heading on a page (the first field that gives one wins). */
|
|
21
29
|
readonly sectionDescription?: string;
|
|
@@ -68,7 +76,7 @@ export interface FieldMeta {
|
|
|
68
76
|
readonly subsection?: string;
|
|
69
77
|
}
|
|
70
78
|
/** Meta keys copied from a field's options or a descriptor onto the field. */
|
|
71
|
-
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"];
|
|
72
80
|
/** The meta `source` gives, without undefined keys. */
|
|
73
81
|
export declare function pickMeta(source: FieldMeta | undefined, skip?: readonly string[]): FieldMeta;
|
|
74
82
|
/** Marks a knob as half of a flat spring: two number keys drawn as one spring card. */
|
|
@@ -490,6 +498,7 @@ export declare function range(value: readonly [number, number], options: FieldMe
|
|
|
490
498
|
unit?: string;
|
|
491
499
|
decimals?: number;
|
|
492
500
|
label?: string;
|
|
501
|
+
description?: string;
|
|
493
502
|
group?: string;
|
|
494
503
|
groupLabel?: string;
|
|
495
504
|
section?: string;
|
|
@@ -96,7 +96,7 @@ export interface LabelOptions {
|
|
|
96
96
|
readonly defaultLabel?: string | Readonly<Record<string, string>>;
|
|
97
97
|
/** How a page card shows its group presets (see `GroupPresetDisplay`). */
|
|
98
98
|
readonly groupPresetDisplay?: GroupPresetDisplay;
|
|
99
|
-
/** By group id: one muted line under its card's title
|
|
99
|
+
/** By group id: one muted line under its card's title, over fields' `groupDescription`. */
|
|
100
100
|
readonly groupDescriptions?: Readonly<Record<string, string>>;
|
|
101
101
|
/** By section id: one muted line under its heading on a page, over fields' `sectionDescription`. */
|
|
102
102
|
readonly sectionDescriptions?: Readonly<Record<string, string>>;
|
|
@@ -146,6 +146,20 @@ export interface HeaderOptions {
|
|
|
146
146
|
*/
|
|
147
147
|
readonly buttons?: readonly HeaderButton[];
|
|
148
148
|
}
|
|
149
|
+
/** What's active in a panel (see `MountPanelOptions.onActive`): a card, or one of its rows. */
|
|
150
|
+
export interface PanelActive {
|
|
151
|
+
readonly group: string;
|
|
152
|
+
readonly key?: string;
|
|
153
|
+
}
|
|
154
|
+
/** What a folded card's or section's summary is made of (see `MountPanelOptions.groupSummary`). */
|
|
155
|
+
export interface FoldSummaryInfo {
|
|
156
|
+
/** The group preset its values match (its name), "Custom" when none does; null without any. */
|
|
157
|
+
readonly preset: string | null;
|
|
158
|
+
/** How many of its rows differ from their defaults (a spring's two keys count once). */
|
|
159
|
+
readonly changed: number;
|
|
160
|
+
/** Its value keys. */
|
|
161
|
+
readonly keys: readonly string[];
|
|
162
|
+
}
|
|
149
163
|
export interface MountPanelOptions extends LabelOptions {
|
|
150
164
|
readonly title?: string | false;
|
|
151
165
|
/**
|
|
@@ -253,6 +267,20 @@ export interface MountPanelOptions extends LabelOptions {
|
|
|
253
267
|
readonly sound?: boolean | SoundOptions;
|
|
254
268
|
/** Called once the panel is mounted, with a handle to focus it (a host's "Tune" key). */
|
|
255
269
|
readonly onReady?: (panel: PanelHandle) => void;
|
|
270
|
+
/**
|
|
271
|
+
* Called when the card or row under the pointer or focus changes: `{ group }` for a card's
|
|
272
|
+
* header or body, `{ group, key }` for one of its rows, `null` for nothing (and once more when
|
|
273
|
+
* the panel goes). Whichever of pointer and focus moved last wins; at most once a frame, never
|
|
274
|
+
* the same value twice. For outlining the part of the app a card changes.
|
|
275
|
+
*/
|
|
276
|
+
readonly onActive?: (at: PanelActive | null) => void;
|
|
277
|
+
/**
|
|
278
|
+
* What a folded group card's header says on its right. In a panel, by default, a one-row card's
|
|
279
|
+
* value ("CSS orb"); otherwise its matching group preset, else its first changed value, else
|
|
280
|
+
* its first choice, then "· N changed". On a page, "<matching preset, or Custom> · N changed".
|
|
281
|
+
* Each part shows only where it applies; `null` shows nothing.
|
|
282
|
+
*/
|
|
283
|
+
readonly groupSummary?: (group: string, info: FoldSummaryInfo) => string | Node | null;
|
|
256
284
|
/** What Copy (and ⇧C) puts on the clipboard; `controls.code()` by default. */
|
|
257
285
|
readonly copy?: () => string;
|
|
258
286
|
/**
|
package/dist/types/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { colorHex, parseColor, type Oklch } from "./core/color.js";
|
|
|
5
5
|
export { sampleGradient, sampleCurve, gradientCss, CURVE_SHAPES, type GradientStop, type GradientSpace, type CurvePoint, type CurveInterp, type SampleGradientOptions, type SampleCurveOptions, } from "./core/ramp.js";
|
|
6
6
|
export { bezierCss, bezierPoint, EASINGS, type Bezier, type EasingName } from "./core/easing.js";
|
|
7
7
|
export { AROUND } from "./core/random.js";
|
|
8
|
-
export { defineControls, type ChangeMeta, type GestureEvent, type ChangeSource, type ChipOptions, type ControlAction, type Controls, type ControlsOptions, type DensityMode, type GroupPreset, type GroupPresetDisplay, type PresetRef, type HeaderButton, type HeaderOptions, type HostHistory, type LabelOptions, type MountPanelOptions, type PanelCommand, type PanelHandle, type PanelModule, type PanelKeys, type PersistMode, type SearchChip, type SearchOptions, type SyncOptions, type RendererMode, type RevealMode, type SectionFolded, type SoundOptions, type SoundTheme, type ThemeMode, type Appearance, } from "./core/store.js";
|
|
8
|
+
export { defineControls, type ChangeMeta, type GestureEvent, type ChangeSource, type ChipOptions, type ControlAction, type Controls, type ControlsOptions, type DensityMode, type FoldSummaryInfo, type GroupPreset, type GroupPresetDisplay, type PresetRef, type HeaderButton, type HeaderOptions, type HostHistory, type LabelOptions, type MountPanelOptions, type PanelActive, type PanelCommand, type PanelHandle, type PanelModule, type PanelKeys, type PersistMode, type SearchChip, type SearchOptions, type SyncOptions, type RendererMode, type RevealMode, type SectionFolded, type SoundOptions, type SoundTheme, type ThemeMode, type Appearance, } from "./core/store.js";
|
|
9
9
|
export { createRemoteControls, descriptorToField, formatSwift, fromDescriptors, fromSettings, type SettingsGroup, type SettingsItem, type ActionDescriptor, type ChoiceOption, type ControlDescriptor, type SpringBindingDescriptor, type ToolbarDescriptor, type ReadoutDescriptor, type ValueDescriptor, type RemoteControls, type RemoteControlsOptions, } from "./core/remote.js";
|
|
10
10
|
export type { LogEntry, LogLevel, TableRows, WatchValue } from "./core/readout.js";
|
|
11
11
|
export { memoryStorage, urlFlag, type StoragePort } from "./core/storage.js";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PanelActive } from "../core/store.js";
|
|
2
|
+
/**
|
|
3
|
+
* What's active in a panel, from where the pointer is and where focus is, each told as it moves.
|
|
4
|
+
* Whichever last moved onto something leads; while it's on nothing (or off the panel), the
|
|
5
|
+
* other's target stands. `report` hears the result at most once a frame (`schedule`), and never
|
|
6
|
+
* the same value twice in a row.
|
|
7
|
+
*/
|
|
8
|
+
export declare function activeReporter<T>(resolve: (target: T | null) => PanelActive | null, report: (at: PanelActive | null) => void, schedule: (flush: () => void) => () => void): {
|
|
9
|
+
pointer(target: T | null): void;
|
|
10
|
+
focus(target: T | null): void;
|
|
11
|
+
/** Stops listening; what was active is no longer, so a last `null` is reported. */
|
|
12
|
+
dispose(): void;
|
|
13
|
+
};
|
|
14
|
+
/** Where an event's node is: in the panel, in one of its menus (on the body), or elsewhere. */
|
|
15
|
+
export type ActiveWhere = "inside" | "menu" | "outside";
|
|
16
|
+
/**
|
|
17
|
+
* The panel's pointer and focus events, told to `reporter` (see `activeReporter`). Focus that
|
|
18
|
+
* moves into one of the panel's menus keeps the row that opened it until it comes back, moves
|
|
19
|
+
* elsewhere, or leaves the menu for nowhere (a blur, the menu closing without handing it back).
|
|
20
|
+
* A pointer that moves (not just one the page scrolled under) takes the lead back from focus.
|
|
21
|
+
*/
|
|
22
|
+
export declare function activeEvents<T>(reporter: {
|
|
23
|
+
pointer(target: T | null): void;
|
|
24
|
+
focus(target: T | null): void;
|
|
25
|
+
}, where: (node: T | null) => ActiveWhere): {
|
|
26
|
+
over(e: {
|
|
27
|
+
readonly target: T | null;
|
|
28
|
+
}): void;
|
|
29
|
+
move(e: {
|
|
30
|
+
readonly target: T | null;
|
|
31
|
+
readonly movementX: number;
|
|
32
|
+
readonly movementY: number;
|
|
33
|
+
}): void;
|
|
34
|
+
out(e: {
|
|
35
|
+
readonly relatedTarget: T | null;
|
|
36
|
+
}): void;
|
|
37
|
+
/** On the document: focus arriving anywhere. */
|
|
38
|
+
focusIn(e: {
|
|
39
|
+
readonly target: T | null;
|
|
40
|
+
}): void;
|
|
41
|
+
/** On the document: focus going nowhere (the window losing it, a blur) fires no focusin. */
|
|
42
|
+
focusOut(e: {
|
|
43
|
+
readonly target: T | null;
|
|
44
|
+
readonly relatedTarget: T | null;
|
|
45
|
+
}): void;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Tells `report` what the pointer or focus is on in `root` (see `activeReporter` and
|
|
49
|
+
* `activeEvents`), resolving the element under them with `resolve`. Returns a cleanup.
|
|
50
|
+
*/
|
|
51
|
+
export declare function watchActive(root: HTMLElement, resolve: (node: Element | null) => PanelActive | null, report: (at: PanelActive | null) => void): {
|
|
52
|
+
dispose(): void;
|
|
53
|
+
};
|
|
@@ -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;
|
|
@@ -399,9 +399,9 @@ export interface GpuGroupBinding {
|
|
|
399
399
|
readonly peek?: HTMLElement;
|
|
400
400
|
/** On a page: the header's controls (presets, reset, die), hidden while the card is folded. */
|
|
401
401
|
readonly controls?: HTMLElement;
|
|
402
|
-
/**
|
|
402
|
+
/** What a folded card's header says (on a page, in place of its controls). */
|
|
403
403
|
readonly summary?: HTMLElement;
|
|
404
|
-
/**
|
|
404
|
+
/** The muted line under the card's title. */
|
|
405
405
|
readonly desc?: HTMLElement;
|
|
406
406
|
/** On a page: the group's presets, as named or numbered segments, or a compact menu. */
|
|
407
407
|
readonly presets?: {
|
|
@@ -732,6 +732,12 @@ declare class GpuOverlayManager {
|
|
|
732
732
|
private clipCache;
|
|
733
733
|
/** Whether the DOM lays `el`'s text out wider than its box (so text-overflow shows "…"). */
|
|
734
734
|
private domClipped;
|
|
735
|
+
private lineCache;
|
|
736
|
+
/**
|
|
737
|
+
* The lines the DOM wraps `el`'s text into, each with its middle's offset from `el`'s top, read
|
|
738
|
+
* back word by word (kept until the text or the width changes).
|
|
739
|
+
*/
|
|
740
|
+
private linesOf;
|
|
735
741
|
private curReduced;
|
|
736
742
|
private curLocks;
|
|
737
743
|
/** The current row's selection wash (0…1), which its hover wash gives way to. */
|
|
@@ -5,6 +5,7 @@ export { computeHintPrefixes, type HintBadgeState, type HintCandidate, type Hint
|
|
|
5
5
|
export { assignPageCodes, hints, pageHints, type HintPainter, type HintSourceOptions, type PageHints, type PageHintTarget, } from "./page-hints.js";
|
|
6
6
|
export { Spring, VelocityTracker } from "./motion.js";
|
|
7
7
|
export { createPanel, mountPage, mountPanel, type Panel, type PanelSearch } from "./panel.js";
|
|
8
|
+
export type { PanelActive } from "../core/store.js";
|
|
8
9
|
export { balancedRuns, type FoldSummaryInfo, type MountPageOptions, type Page, type PageAction, type PageCard, type PageCardOptions, type PageControl, type PageFoldOptions, type PageFoldSource, type PageGroupInfo, type PageHighlight, type PageSectionInfo, } from "./page.js";
|
|
9
10
|
export { createPresetStrip, nextPresetIndex, resolveActivePresetEntry, type ActivePresetState, type PresetEntry, type PresetStrip, } from "./presets.js";
|
|
10
11
|
export { sound, type CueDetail, type Sound, type SoundCue, type SoundLayer, type SoundOptions, type SoundTheme, } from "./sound.js";
|
|
@@ -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,15 +1,7 @@
|
|
|
1
1
|
import type { StoragePort } from "../core/storage.js";
|
|
2
|
-
import type { MountPanelOptions, SectionFolded } from "../core/store.js";
|
|
2
|
+
import type { FoldSummaryInfo, MountPanelOptions, SectionFolded } from "../core/store.js";
|
|
3
3
|
import type { Panel } from "./panel.js";
|
|
4
|
-
|
|
5
|
-
export interface FoldSummaryInfo {
|
|
6
|
-
/** The group preset its values match (its name), "Custom" when none does; null without any. */
|
|
7
|
-
readonly preset: string | null;
|
|
8
|
-
/** How many of its rows differ from their defaults (a spring's two keys count once). */
|
|
9
|
-
readonly changed: number;
|
|
10
|
-
/** Its value keys. */
|
|
11
|
-
readonly keys: readonly string[];
|
|
12
|
-
}
|
|
4
|
+
export type { FoldSummaryInfo };
|
|
13
5
|
export interface MountPageOptions extends MountPanelOptions {
|
|
14
6
|
/** Section order; sections not listed follow in the order they first appear. */
|
|
15
7
|
readonly sections?: readonly string[];
|
|
@@ -39,11 +31,6 @@ export interface MountPageOptions extends MountPanelOptions {
|
|
|
39
31
|
* opening it if it's folded.
|
|
40
32
|
*/
|
|
41
33
|
readonly nav?: boolean;
|
|
42
|
-
/**
|
|
43
|
-
* What a folded group card's header says. By default "<matching preset, or Custom> · N
|
|
44
|
-
* changed", leaving out what doesn't apply. `null` shows nothing.
|
|
45
|
-
*/
|
|
46
|
-
readonly groupSummary?: (group: string, info: FoldSummaryInfo) => string | Node | null;
|
|
47
34
|
/** What a folded section's heading says ("N changed · M cards" by default; `preset` is null). */
|
|
48
35
|
readonly sectionSummary?: (section: string, info: FoldSummaryInfo) => string | Node | null;
|
|
49
36
|
}
|
|
@@ -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;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Controls } from "../core/store.js";
|
|
2
|
+
/** A row of a folded panel card, as its summary reads it. */
|
|
3
|
+
export interface SummaryRow {
|
|
4
|
+
/** Its value as text ("CSS orb", "12 px"), or "" where it has no short form. */
|
|
5
|
+
readonly value: string;
|
|
6
|
+
/** Whether it differs from its default. */
|
|
7
|
+
readonly changed: boolean;
|
|
8
|
+
/** Whether it's a choice (the likeliest to say what the card is set to). */
|
|
9
|
+
readonly choice: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A folded panel card's summary, by default: a one-row card's value; otherwise the most telling
|
|
13
|
+
* value (its matching group preset, else its first changed value, else its first choice) and
|
|
14
|
+
* "N changed", each only where it applies. `preset` is null for a card without group presets.
|
|
15
|
+
*/
|
|
16
|
+
export declare function panelFoldSummary(rows: readonly SummaryRow[], preset: string | null): string;
|
|
17
|
+
/**
|
|
18
|
+
* Which presets the values are measured against (the panel's, each scope's and each group's).
|
|
19
|
+
* It changes when one is chosen or saved even if no value moves, which still changes what counts
|
|
20
|
+
* as changed: marks, resets and folded summaries follow it.
|
|
21
|
+
*/
|
|
22
|
+
export declare const baseSignature: (controls: Pick<Controls<any>, "activePreset" | "activeGroupPreset" | "scopes" | "groupPresets">) => string;
|
|
@@ -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
|
*/
|