@kgdiff/viewer 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -16
- package/custom-elements.json +2347 -0
- package/dist/bind.d.ts +41 -0
- package/dist/bind.d.ts.map +1 -0
- package/dist/bind.js +47 -0
- package/dist/bind.js.map +1 -0
- package/dist/controller.d.ts +22 -74
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +35 -376
- package/dist/controller.js.map +1 -1
- package/dist/dom.d.ts +10 -0
- package/dist/dom.d.ts.map +1 -1
- package/dist/dom.js +29 -0
- package/dist/dom.js.map +1 -1
- package/dist/element.d.ts +52 -46
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +362 -403
- package/dist/element.js.map +1 -1
- package/dist/elements/actions.d.ts +27 -0
- package/dist/elements/actions.d.ts.map +1 -0
- package/dist/elements/actions.js +81 -0
- package/dist/elements/actions.js.map +1 -0
- package/dist/elements/bound.d.ts +63 -0
- package/dist/elements/bound.d.ts.map +1 -0
- package/dist/elements/bound.js +213 -0
- package/dist/elements/bound.js.map +1 -0
- package/dist/elements/change-detail.d.ts +25 -0
- package/dist/elements/change-detail.d.ts.map +1 -0
- package/dist/elements/change-detail.js +19 -0
- package/dist/elements/change-detail.js.map +1 -0
- package/dist/elements/change-list.d.ts +27 -0
- package/dist/elements/change-list.d.ts.map +1 -0
- package/dist/elements/change-list.js +29 -0
- package/dist/elements/change-list.js.map +1 -0
- package/dist/elements/filters.d.ts +25 -0
- package/dist/elements/filters.d.ts.map +1 -0
- package/dist/elements/filters.js +19 -0
- package/dist/elements/filters.js.map +1 -0
- package/dist/elements/legend.d.ts +40 -0
- package/dist/elements/legend.d.ts.map +1 -0
- package/dist/elements/legend.js +87 -0
- package/dist/elements/legend.js.map +1 -0
- package/dist/elements/minimap.d.ts +38 -0
- package/dist/elements/minimap.d.ts.map +1 -0
- package/dist/elements/minimap.js +59 -0
- package/dist/elements/minimap.js.map +1 -0
- package/dist/elements/root.d.ts +40 -0
- package/dist/elements/root.d.ts.map +1 -0
- package/dist/elements/root.js +67 -0
- package/dist/elements/root.js.map +1 -0
- package/dist/elements/search.d.ts +29 -0
- package/dist/elements/search.d.ts.map +1 -0
- package/dist/elements/search.js +23 -0
- package/dist/elements/search.js.map +1 -0
- package/dist/elements/stage-context.d.ts +31 -0
- package/dist/elements/stage-context.d.ts.map +1 -0
- package/dist/elements/stage-context.js +4 -0
- package/dist/elements/stage-context.js.map +1 -0
- package/dist/elements/stage.d.ts +65 -0
- package/dist/elements/stage.d.ts.map +1 -0
- package/dist/elements/stage.js +259 -0
- package/dist/elements/stage.js.map +1 -0
- package/dist/elements/title.d.ts +25 -0
- package/dist/elements/title.d.ts.map +1 -0
- package/dist/elements/title.js +19 -0
- package/dist/elements/title.js.map +1 -0
- package/dist/elements/transport.d.ts +29 -0
- package/dist/elements/transport.d.ts.map +1 -0
- package/dist/elements/transport.js +24 -0
- package/dist/elements/transport.js.map +1 -0
- package/dist/elements/view-bound.d.ts +39 -0
- package/dist/elements/view-bound.d.ts.map +1 -0
- package/dist/elements/view-bound.js +88 -0
- package/dist/elements/view-bound.js.map +1 -0
- package/dist/elements/view-dock.d.ts +28 -0
- package/dist/elements/view-dock.d.ts.map +1 -0
- package/dist/elements/view-dock.js +28 -0
- package/dist/elements/view-dock.js.map +1 -0
- package/dist/elements/zoom.d.ts +28 -0
- package/dist/elements/zoom.d.ts.map +1 -0
- package/dist/elements/zoom.js +22 -0
- package/dist/elements/zoom.js.map +1 -0
- package/dist/i18n.d.ts +10 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +20 -0
- package/dist/i18n.js.map +1 -1
- package/dist/icons.d.ts +1 -0
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js +1 -0
- package/dist/icons.js.map +1 -1
- package/dist/index.d.ts +328 -34
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/keyboard.d.ts +39 -2
- package/dist/keyboard.d.ts.map +1 -1
- package/dist/keyboard.js +145 -62
- package/dist/keyboard.js.map +1 -1
- package/dist/layout-store.d.ts +26 -0
- package/dist/layout-store.d.ts.map +1 -0
- package/dist/layout-store.js +46 -0
- package/dist/layout-store.js.map +1 -0
- package/dist/panel-levels.d.ts +18 -0
- package/dist/panel-levels.d.ts.map +1 -0
- package/dist/panel-levels.js +41 -0
- package/dist/panel-levels.js.map +1 -0
- package/dist/panels.d.ts +28 -0
- package/dist/panels.d.ts.map +1 -0
- package/dist/panels.js +73 -0
- package/dist/panels.js.map +1 -0
- package/dist/prefs.d.ts +5 -2
- package/dist/prefs.d.ts.map +1 -1
- package/dist/prefs.js +12 -3
- package/dist/prefs.js.map +1 -1
- package/dist/react.d.ts +460 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +2 -0
- package/dist/react.js.map +1 -0
- package/dist/root-element.d.ts +122 -0
- package/dist/root-element.d.ts.map +1 -0
- package/dist/root-element.js +541 -0
- package/dist/root-element.js.map +1 -0
- package/dist/root.d.ts +30 -30
- package/dist/root.d.ts.map +1 -1
- package/dist/root.js +65 -42
- package/dist/root.js.map +1 -1
- package/dist/session.d.ts +19 -3
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +15 -17
- package/dist/session.js.map +1 -1
- package/dist/standalone/kgdiff-viewer.js +4 -4
- package/dist/styles.d.ts +4 -0
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +224 -84
- package/dist/styles.js.map +1 -1
- package/dist/theme.d.ts +7 -0
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +10 -7
- package/dist/theme.js.map +1 -1
- package/dist/topics.d.ts +8 -0
- package/dist/topics.d.ts.map +1 -0
- package/dist/topics.js +18 -0
- package/dist/topics.js.map +1 -0
- package/dist/ui/change-list.d.ts.map +1 -1
- package/dist/ui/change-list.js +2 -2
- package/dist/ui/change-list.js.map +1 -1
- package/dist/ui/context.d.ts +20 -7
- package/dist/ui/context.d.ts.map +1 -1
- package/dist/ui/context.js.map +1 -1
- package/dist/ui/header.d.ts +41 -3
- package/dist/ui/header.d.ts.map +1 -1
- package/dist/ui/header.js +99 -34
- package/dist/ui/header.js.map +1 -1
- package/dist/ui/legend.d.ts +15 -0
- package/dist/ui/legend.d.ts.map +1 -0
- package/dist/ui/legend.js +68 -0
- package/dist/ui/legend.js.map +1 -0
- package/dist/ui/popovers.d.ts +3 -1
- package/dist/ui/popovers.d.ts.map +1 -1
- package/dist/ui/popovers.js +62 -23
- package/dist/ui/popovers.js.map +1 -1
- package/dist/ui/search.d.ts.map +1 -1
- package/dist/ui/search.js +3 -2
- package/dist/ui/search.js.map +1 -1
- package/dist/ui/sidebar.d.ts +26 -3
- package/dist/ui/sidebar.d.ts.map +1 -1
- package/dist/ui/sidebar.js +344 -12
- package/dist/ui/sidebar.js.map +1 -1
- package/dist/ui/stage.d.ts +56 -1
- package/dist/ui/stage.d.ts.map +1 -1
- package/dist/ui/stage.js +244 -256
- package/dist/ui/stage.js.map +1 -1
- package/dist/ui/transport.d.ts +12 -0
- package/dist/ui/transport.d.ts.map +1 -0
- package/dist/ui/transport.js +198 -0
- package/dist/ui/transport.js.map +1 -0
- package/dist/ui/zoom.d.ts +15 -0
- package/dist/ui/zoom.d.ts.map +1 -0
- package/dist/ui/zoom.js +30 -0
- package/dist/ui/zoom.js.map +1 -0
- package/dist/view.d.ts +94 -0
- package/dist/view.d.ts.map +1 -0
- package/dist/view.js +441 -0
- package/dist/view.js.map +1 -0
- package/package.json +95 -12
- package/src/bind.ts +80 -0
- package/src/controller.ts +51 -442
- package/src/dom.ts +30 -0
- package/src/element.ts +364 -406
- package/src/elements/actions.ts +94 -0
- package/src/elements/bound.ts +247 -0
- package/src/elements/change-detail.ts +34 -0
- package/src/elements/change-list.ts +51 -0
- package/src/elements/filters.ts +34 -0
- package/src/elements/legend.ts +103 -0
- package/src/elements/minimap.ts +75 -0
- package/src/elements/root.ts +86 -0
- package/src/elements/search.ts +38 -0
- package/src/elements/stage-context.ts +34 -0
- package/src/elements/stage.ts +285 -0
- package/src/elements/title.ts +34 -0
- package/src/elements/transport.ts +39 -0
- package/src/elements/view-bound.ts +118 -0
- package/src/elements/view-dock.ts +43 -0
- package/src/elements/zoom.ts +38 -0
- package/src/i18n.ts +20 -0
- package/src/icons.ts +1 -0
- package/src/index.ts +347 -34
- package/src/keyboard.ts +188 -61
- package/src/layout-store.ts +71 -0
- package/src/panel-levels.ts +53 -0
- package/src/panels.ts +81 -0
- package/src/prefs.ts +12 -4
- package/src/react.ts +459 -0
- package/src/root-element.ts +624 -0
- package/src/root.ts +92 -65
- package/src/session.ts +22 -33
- package/src/styles.ts +245 -84
- package/src/theme.ts +10 -7
- package/src/topics.ts +37 -0
- package/src/ui/change-list.ts +6 -2
- package/src/ui/context.ts +20 -7
- package/src/ui/header.ts +127 -56
- package/src/ui/legend.ts +112 -0
- package/src/ui/popovers.ts +68 -25
- package/src/ui/search.ts +10 -5
- package/src/ui/sidebar.ts +386 -23
- package/src/ui/stage.ts +267 -331
- package/src/ui/transport.ts +240 -0
- package/src/ui/zoom.ts +45 -0
- package/src/view.ts +516 -0
- package/vscode.html-custom-data.json +726 -0
package/src/root.ts
CHANGED
|
@@ -4,44 +4,55 @@
|
|
|
4
4
|
* they share: the keyboard map, the live region, the events, the focus targets and the theme
|
|
5
5
|
* tokens. Regions never reference each other; everything goes through the session or the root.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* own.
|
|
7
|
+
* Its host is `<kgdiff-viewer>` (the default layout, whose regions render into its shadow
|
|
8
|
+
* root) or `<kgdiff-root>` (whose elements are placed by the page).
|
|
10
9
|
*/
|
|
11
10
|
import type { Theme } from '@kgdiff/scene';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import type { Region, ViewerContext } from './bind.js';
|
|
12
|
+
import { viewerContext } from './bind.js';
|
|
13
|
+
import { provideContext } from './context.js';
|
|
14
14
|
import { h } from './dom.js';
|
|
15
15
|
import type { ThemePreference } from './index.js';
|
|
16
|
-
import { isTypingTarget, keyAction } from './keyboard.js';
|
|
16
|
+
import { isTypingTarget, type Keymap, keyAction } from './keyboard.js';
|
|
17
17
|
import { THEME_PREFERENCES } from './prefs.js';
|
|
18
18
|
import { defaultStorage } from './review-store.js';
|
|
19
19
|
import { cancelFrame, nextPaint, requestFrame } from './schedule.js';
|
|
20
20
|
import type { Topic } from './session.js';
|
|
21
|
+
import { DiffSession, type SessionDeps, type SessionHost } from './session.js';
|
|
21
22
|
import { stylesheet } from './styles.js';
|
|
22
23
|
import { themeFromStyle, tokenSheet } from './theme.js';
|
|
23
|
-
import type { FocusTarget, UiActions
|
|
24
|
+
import type { FocusTarget, UiActions } from './ui/context.js';
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
export
|
|
27
|
-
|
|
26
|
+
export type { Region, ViewerContext } from './bind.js';
|
|
27
|
+
export { bindRegion, uiContext, viewerContext, wasForwarded } from './bind.js';
|
|
28
|
+
|
|
29
|
+
/** Hosts of roots (the nearest root acts on a key; an outer one leaves it alone). */
|
|
30
|
+
const rootHosts = new WeakSet<EventTarget>();
|
|
31
|
+
|
|
32
|
+
/** Whether `target` is the host of a root. */
|
|
33
|
+
export function isRoot(target: EventTarget): boolean {
|
|
34
|
+
return rootHosts.has(target);
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
/** What
|
|
31
|
-
export interface
|
|
32
|
-
|
|
33
|
-
readonly
|
|
34
|
-
/** The
|
|
35
|
-
readonly
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
/** The keyboard map, for a key event that reached the root. */
|
|
41
|
-
handleKey(e: KeyboardEvent): void;
|
|
37
|
+
/** What the element that owns a root adds to it. */
|
|
38
|
+
export interface RootOptions<S extends DiffSession = DiffSession> {
|
|
39
|
+
/** The session to make (default: a plain `DiffSession`, without a canvas of its own). */
|
|
40
|
+
readonly session?: (host: SessionHost, deps: SessionDeps) => S;
|
|
41
|
+
/** The sheets the root's shadow root adopts before its token set (default: the viewer's). */
|
|
42
|
+
readonly sheets?: (doc: Document) => readonly CSSStyleSheet[];
|
|
43
|
+
/** A key forwarded by an element bound with `for` (default: the keyboard map). */
|
|
44
|
+
readonly key?: (e: KeyboardEvent) => void;
|
|
45
|
+
/** The pointer entered or left the graph. */
|
|
46
|
+
readonly hover?: (over: boolean) => void;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
/** The shortcuts a focused list keeps with `shortcuts="none"` (the listbox's own keys). */
|
|
50
|
+
const LIST_KEYS: ReadonlySet<string> = new Set(['next', 'prev', 'first', 'last', 'fly']);
|
|
51
|
+
|
|
52
|
+
interface Target {
|
|
53
|
+
readonly focus: () => void;
|
|
54
|
+
readonly available: () => boolean;
|
|
55
|
+
}
|
|
45
56
|
|
|
46
57
|
function media(query: string): MediaQueryList | null {
|
|
47
58
|
return typeof window !== 'undefined' && typeof window.matchMedia === 'function'
|
|
@@ -49,16 +60,23 @@ function media(query: string): MediaQueryList | null {
|
|
|
49
60
|
: null;
|
|
50
61
|
}
|
|
51
62
|
|
|
52
|
-
export class ViewerRoot implements ViewerContext {
|
|
53
|
-
readonly controller:
|
|
63
|
+
export class ViewerRoot<S extends DiffSession = DiffSession> implements ViewerContext {
|
|
64
|
+
readonly controller: S;
|
|
54
65
|
/** The single polite live region announcing navigation ("Change 3 of 12: …"). */
|
|
55
66
|
readonly live = h('div', { class: 'sr-only', 'aria-live': 'polite', 'aria-atomic': 'true' });
|
|
56
67
|
readonly host: HTMLElement;
|
|
57
68
|
readonly actions: UiActions;
|
|
58
69
|
readonly #shadow: ShadowRoot;
|
|
59
70
|
readonly #regions: Region[] = [];
|
|
60
|
-
readonly #targets = new Map<FocusTarget,
|
|
71
|
+
readonly #targets = new Map<FocusTarget, Target[]>();
|
|
72
|
+
readonly #options: RootOptions<S>;
|
|
61
73
|
#renderFailed = false;
|
|
74
|
+
/** The built-in theme of the token set applied last. */
|
|
75
|
+
#base: Theme | null = null;
|
|
76
|
+
/** `keymap`: single keys bound to other shortcuts, or unbound. */
|
|
77
|
+
keymap: Keymap | null = null;
|
|
78
|
+
/** `shortcuts="none"`: no shortcuts, but a focused list keeps its own keys. */
|
|
79
|
+
shortcutsOff = false;
|
|
62
80
|
|
|
63
81
|
/**
|
|
64
82
|
* `host` dispatches the events and computes the tokens set on `shadow` (its shadow root);
|
|
@@ -69,15 +87,18 @@ export class ViewerRoot implements ViewerContext {
|
|
|
69
87
|
shadow: ShadowRoot,
|
|
70
88
|
actions: UiActions,
|
|
71
89
|
emitted?: (type: string) => void,
|
|
90
|
+
options: RootOptions<S> = {},
|
|
72
91
|
) {
|
|
73
92
|
this.host = host;
|
|
74
93
|
this.#shadow = shadow;
|
|
75
94
|
this.actions = actions;
|
|
95
|
+
this.#options = options;
|
|
96
|
+
rootHosts.add(host);
|
|
76
97
|
const emit = (type: string, detail: unknown): void => {
|
|
77
98
|
host.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
|
|
78
99
|
emitted?.(type);
|
|
79
100
|
};
|
|
80
|
-
const events:
|
|
101
|
+
const events: SessionHost = {
|
|
81
102
|
ready: (stats) => emit('kgdiff-ready', { stats }),
|
|
82
103
|
select: (hunk, node) => emit('kgdiff-select', { hunk, node }),
|
|
83
104
|
mode: (mode, scope) => emit('kgdiff-mode', { mode, scope }),
|
|
@@ -86,7 +107,7 @@ export class ViewerRoot implements ViewerContext {
|
|
|
86
107
|
announce: (text) => this.announce(text),
|
|
87
108
|
replayEnd: (loop) => emit('kgdiff-replay-end', { loop }),
|
|
88
109
|
};
|
|
89
|
-
|
|
110
|
+
const deps: SessionDeps = {
|
|
90
111
|
now: () => (typeof performance !== 'undefined' ? performance.now() : Date.now()),
|
|
91
112
|
requestFrame,
|
|
92
113
|
cancelFrame,
|
|
@@ -99,7 +120,10 @@ export class ViewerRoot implements ViewerContext {
|
|
|
99
120
|
const view = host.ownerDocument.defaultView;
|
|
100
121
|
return view ? themeFromStyle(base, view.getComputedStyle(host)) : base;
|
|
101
122
|
},
|
|
102
|
-
}
|
|
123
|
+
};
|
|
124
|
+
this.controller = options.session
|
|
125
|
+
? options.session(events, deps)
|
|
126
|
+
: (new DiffSession(events, deps) as S);
|
|
103
127
|
this.controller.subscribe((topics) => this.render(topics));
|
|
104
128
|
}
|
|
105
129
|
|
|
@@ -139,20 +163,27 @@ export class ViewerRoot implements ViewerContext {
|
|
|
139
163
|
};
|
|
140
164
|
}
|
|
141
165
|
|
|
142
|
-
target(name: FocusTarget, focus: () => void): () => void {
|
|
166
|
+
target(name: FocusTarget, focus: () => void, available?: () => boolean): () => void {
|
|
143
167
|
const list = this.#targets.get(name) ?? [];
|
|
144
168
|
this.#targets.set(name, list);
|
|
145
|
-
|
|
169
|
+
const target: Target = { focus, available: available ?? (() => true) };
|
|
170
|
+
list.push(target);
|
|
146
171
|
return () => {
|
|
147
|
-
const i = list.indexOf(
|
|
172
|
+
const i = list.indexOf(target);
|
|
148
173
|
if (i >= 0) list.splice(i, 1);
|
|
149
174
|
};
|
|
150
175
|
}
|
|
151
176
|
|
|
177
|
+
/** Focus the `name` target registered last of those available (in an open region). */
|
|
152
178
|
focus(name: FocusTarget): boolean {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
179
|
+
const list = this.#targets.get(name) ?? [];
|
|
180
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
181
|
+
const target = list[i] as Target;
|
|
182
|
+
if (!target.available()) continue;
|
|
183
|
+
target.focus();
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
156
187
|
}
|
|
157
188
|
|
|
158
189
|
announce(text: string): void {
|
|
@@ -183,19 +214,42 @@ export class ViewerRoot implements ViewerContext {
|
|
|
183
214
|
|
|
184
215
|
/** Apply the token set of `base` (a built-in theme) on the host, in the host's document. */
|
|
185
216
|
#tokens(base: Theme): void {
|
|
217
|
+
this.#base = base;
|
|
186
218
|
const doc = this.host.ownerDocument;
|
|
187
219
|
const tokens = tokenSheet(base, doc);
|
|
188
|
-
if (this.#shadow.adoptedStyleSheets
|
|
189
|
-
this.#
|
|
220
|
+
if (this.#shadow.adoptedStyleSheets.at(-1) !== tokens) {
|
|
221
|
+
const sheets = this.#options.sheets?.(doc) ?? [stylesheet(doc)];
|
|
222
|
+
this.#shadow.adoptedStyleSheets = [...sheets, tokens];
|
|
190
223
|
}
|
|
191
224
|
}
|
|
192
225
|
|
|
226
|
+
tokens(doc: Document): CSSStyleSheet {
|
|
227
|
+
// Resolving the theme applies the token set (and remembers its base).
|
|
228
|
+
if (!this.#base) void this.controller.theme;
|
|
229
|
+
return tokenSheet(this.#base ?? this.controller.theme, doc);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
forwardKey(e: KeyboardEvent): void {
|
|
233
|
+
if (this.#options.key) this.#options.key(e);
|
|
234
|
+
else this.handleKey(e);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
hover(over: boolean): void {
|
|
238
|
+
this.#options.hover?.(over);
|
|
239
|
+
}
|
|
240
|
+
|
|
193
241
|
handleKey(e: KeyboardEvent): void {
|
|
194
242
|
const target = e.composedPath()[0] ?? null;
|
|
195
243
|
if (isTypingTarget(target)) return;
|
|
196
244
|
const c = this.controller;
|
|
197
|
-
const action = keyAction(e, c.mode, c.scope);
|
|
245
|
+
const action = keyAction(e, c.mode, c.scope, this.keymap);
|
|
198
246
|
if (!action) return;
|
|
247
|
+
if (
|
|
248
|
+
this.shortcutsOff &&
|
|
249
|
+
!(LIST_KEYS.has(action.type) && (target as Element | null)?.closest?.('[role="listbox"]'))
|
|
250
|
+
) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
199
253
|
// Enter / Space on a focused control activate that control.
|
|
200
254
|
const el = target as Element | null;
|
|
201
255
|
if (
|
|
@@ -279,30 +333,3 @@ export class ViewerRoot implements ViewerContext {
|
|
|
279
333
|
}
|
|
280
334
|
}
|
|
281
335
|
}
|
|
282
|
-
|
|
283
|
-
/** The UI context of a region placed in `root` (its shadow tree), bound to `ctx`. */
|
|
284
|
-
export function uiContext(ctx: ViewerContext, root: ShadowRoot): UiContext {
|
|
285
|
-
return {
|
|
286
|
-
...ctx.actions,
|
|
287
|
-
controller: ctx.controller,
|
|
288
|
-
root,
|
|
289
|
-
target: (name, focus) => ctx.target(name, focus),
|
|
290
|
-
focus: (name) => ctx.focus(name),
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Bind a region to the root nearest `anchor` (an element where the region is placed): `make`
|
|
296
|
-
* builds it once the root answers, and the root renders it from then on.
|
|
297
|
-
*/
|
|
298
|
-
export function bindRegion<R extends Region>(anchor: Element, make: (ui: UiContext) => R): R {
|
|
299
|
-
const bound: { region?: R } = {};
|
|
300
|
-
requestContext(anchor, viewerContext, (ctx) => {
|
|
301
|
-
if (bound.region) return;
|
|
302
|
-
bound.region = make(uiContext(ctx, anchor.getRootNode() as ShadowRoot));
|
|
303
|
-
ctx.attach(bound.region);
|
|
304
|
-
});
|
|
305
|
-
// Phase 1: regions are bound by the layout that owns the root, which answers at once.
|
|
306
|
-
if (!bound.region) throw new Error('[kgdiff] a region found no root to bind to.');
|
|
307
|
-
return bound.region;
|
|
308
|
-
}
|
package/src/session.ts
CHANGED
|
@@ -50,42 +50,12 @@ import { LEGACY_CHANGES_MODE, toLocale, toMode, toPalette, toScope, toTheme } fr
|
|
|
50
50
|
import { ReplayClock } from './replay.js';
|
|
51
51
|
import { ReviewStore, reviewKey, type StorageLike } from './review-store.js';
|
|
52
52
|
import { type SearchHit, SearchIndex } from './search.js';
|
|
53
|
+
import { ALL_TOPICS, type Topic } from './topics.js';
|
|
53
54
|
|
|
54
55
|
export type Status = 'idle' | 'loading' | 'ready' | 'error';
|
|
55
56
|
|
|
56
|
-
export type Topic
|
|
57
|
-
|
|
58
|
-
| 'data'
|
|
59
|
-
| 'mode'
|
|
60
|
-
| 'selection'
|
|
61
|
-
| 'filters'
|
|
62
|
-
| 'search'
|
|
63
|
-
| 'review'
|
|
64
|
-
| 'replay'
|
|
65
|
-
| 'hover'
|
|
66
|
-
| 'theme'
|
|
67
|
-
| 'locale'
|
|
68
|
-
| 'focus'
|
|
69
|
-
| 'swipe'
|
|
70
|
-
| 'overview';
|
|
71
|
-
|
|
72
|
-
/** Every topic: a full render. */
|
|
73
|
-
export const ALL_TOPICS: ReadonlySet<Topic> = new Set<Topic>([
|
|
74
|
-
'status',
|
|
75
|
-
'data',
|
|
76
|
-
'mode',
|
|
77
|
-
'selection',
|
|
78
|
-
'filters',
|
|
79
|
-
'search',
|
|
80
|
-
'review',
|
|
81
|
-
'replay',
|
|
82
|
-
'hover',
|
|
83
|
-
'theme',
|
|
84
|
-
'locale',
|
|
85
|
-
'focus',
|
|
86
|
-
'swipe',
|
|
87
|
-
'overview',
|
|
88
|
-
]);
|
|
57
|
+
export type { Topic } from './topics.js';
|
|
58
|
+
export { ALL_TOPICS } from './topics.js';
|
|
89
59
|
|
|
90
60
|
/** What the session reports (`<kgdiff-viewer>` turns these into events and announcements). */
|
|
91
61
|
export interface SessionHost {
|
|
@@ -136,6 +106,10 @@ export interface SessionView {
|
|
|
136
106
|
looped(): void;
|
|
137
107
|
/** The world region in view, or null before layout. */
|
|
138
108
|
viewport(): Rect | null;
|
|
109
|
+
/** Fit everything visible into view (labels included); views without a camera need none. */
|
|
110
|
+
fit?(options?: { readonly animate?: boolean; readonly padding?: number }): Promise<void>;
|
|
111
|
+
/** Zoom in (1) or out (-1) one step. */
|
|
112
|
+
zoom?(direction: 1 | -1): void;
|
|
139
113
|
}
|
|
140
114
|
|
|
141
115
|
export type Preference = 'mode' | 'scope' | 'theme' | 'palette' | 'locale';
|
|
@@ -260,6 +234,21 @@ export class DiffSession {
|
|
|
260
234
|
return () => this.#views.delete(view);
|
|
261
235
|
}
|
|
262
236
|
|
|
237
|
+
/** @internal The session's clock (`SessionDeps.now`), for its views. */
|
|
238
|
+
now(): number {
|
|
239
|
+
return this.#deps.now();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Fit what is drawn into every view (each frames it on its own canvas). */
|
|
243
|
+
fit(options?: { readonly animate?: boolean; readonly padding?: number }): Promise<void> {
|
|
244
|
+
return Promise.all([...this.#views].map((v) => v.fit?.(options))).then(() => undefined);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Zoom every view one step in (1) or out (-1): the keyboard's `+` / `−`. */
|
|
248
|
+
zoom(direction: 1 | -1): void {
|
|
249
|
+
for (const v of this.#views) v.zoom?.(direction);
|
|
250
|
+
}
|
|
251
|
+
|
|
263
252
|
// ─── Derived state ────────────────────────────────────────────────────────
|
|
264
253
|
|
|
265
254
|
get themeName(): ThemeName {
|