@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.
Files changed (232) hide show
  1. package/README.md +43 -16
  2. package/custom-elements.json +2347 -0
  3. package/dist/bind.d.ts +41 -0
  4. package/dist/bind.d.ts.map +1 -0
  5. package/dist/bind.js +47 -0
  6. package/dist/bind.js.map +1 -0
  7. package/dist/controller.d.ts +22 -74
  8. package/dist/controller.d.ts.map +1 -1
  9. package/dist/controller.js +35 -376
  10. package/dist/controller.js.map +1 -1
  11. package/dist/dom.d.ts +10 -0
  12. package/dist/dom.d.ts.map +1 -1
  13. package/dist/dom.js +29 -0
  14. package/dist/dom.js.map +1 -1
  15. package/dist/element.d.ts +52 -46
  16. package/dist/element.d.ts.map +1 -1
  17. package/dist/element.js +362 -403
  18. package/dist/element.js.map +1 -1
  19. package/dist/elements/actions.d.ts +27 -0
  20. package/dist/elements/actions.d.ts.map +1 -0
  21. package/dist/elements/actions.js +81 -0
  22. package/dist/elements/actions.js.map +1 -0
  23. package/dist/elements/bound.d.ts +63 -0
  24. package/dist/elements/bound.d.ts.map +1 -0
  25. package/dist/elements/bound.js +213 -0
  26. package/dist/elements/bound.js.map +1 -0
  27. package/dist/elements/change-detail.d.ts +25 -0
  28. package/dist/elements/change-detail.d.ts.map +1 -0
  29. package/dist/elements/change-detail.js +19 -0
  30. package/dist/elements/change-detail.js.map +1 -0
  31. package/dist/elements/change-list.d.ts +27 -0
  32. package/dist/elements/change-list.d.ts.map +1 -0
  33. package/dist/elements/change-list.js +29 -0
  34. package/dist/elements/change-list.js.map +1 -0
  35. package/dist/elements/filters.d.ts +25 -0
  36. package/dist/elements/filters.d.ts.map +1 -0
  37. package/dist/elements/filters.js +19 -0
  38. package/dist/elements/filters.js.map +1 -0
  39. package/dist/elements/legend.d.ts +40 -0
  40. package/dist/elements/legend.d.ts.map +1 -0
  41. package/dist/elements/legend.js +87 -0
  42. package/dist/elements/legend.js.map +1 -0
  43. package/dist/elements/minimap.d.ts +38 -0
  44. package/dist/elements/minimap.d.ts.map +1 -0
  45. package/dist/elements/minimap.js +59 -0
  46. package/dist/elements/minimap.js.map +1 -0
  47. package/dist/elements/root.d.ts +40 -0
  48. package/dist/elements/root.d.ts.map +1 -0
  49. package/dist/elements/root.js +67 -0
  50. package/dist/elements/root.js.map +1 -0
  51. package/dist/elements/search.d.ts +29 -0
  52. package/dist/elements/search.d.ts.map +1 -0
  53. package/dist/elements/search.js +23 -0
  54. package/dist/elements/search.js.map +1 -0
  55. package/dist/elements/stage-context.d.ts +31 -0
  56. package/dist/elements/stage-context.d.ts.map +1 -0
  57. package/dist/elements/stage-context.js +4 -0
  58. package/dist/elements/stage-context.js.map +1 -0
  59. package/dist/elements/stage.d.ts +65 -0
  60. package/dist/elements/stage.d.ts.map +1 -0
  61. package/dist/elements/stage.js +259 -0
  62. package/dist/elements/stage.js.map +1 -0
  63. package/dist/elements/title.d.ts +25 -0
  64. package/dist/elements/title.d.ts.map +1 -0
  65. package/dist/elements/title.js +19 -0
  66. package/dist/elements/title.js.map +1 -0
  67. package/dist/elements/transport.d.ts +29 -0
  68. package/dist/elements/transport.d.ts.map +1 -0
  69. package/dist/elements/transport.js +24 -0
  70. package/dist/elements/transport.js.map +1 -0
  71. package/dist/elements/view-bound.d.ts +39 -0
  72. package/dist/elements/view-bound.d.ts.map +1 -0
  73. package/dist/elements/view-bound.js +88 -0
  74. package/dist/elements/view-bound.js.map +1 -0
  75. package/dist/elements/view-dock.d.ts +28 -0
  76. package/dist/elements/view-dock.d.ts.map +1 -0
  77. package/dist/elements/view-dock.js +28 -0
  78. package/dist/elements/view-dock.js.map +1 -0
  79. package/dist/elements/zoom.d.ts +28 -0
  80. package/dist/elements/zoom.d.ts.map +1 -0
  81. package/dist/elements/zoom.js +22 -0
  82. package/dist/elements/zoom.js.map +1 -0
  83. package/dist/i18n.d.ts +10 -0
  84. package/dist/i18n.d.ts.map +1 -1
  85. package/dist/i18n.js +20 -0
  86. package/dist/i18n.js.map +1 -1
  87. package/dist/icons.d.ts +1 -0
  88. package/dist/icons.d.ts.map +1 -1
  89. package/dist/icons.js +1 -0
  90. package/dist/icons.js.map +1 -1
  91. package/dist/index.d.ts +328 -34
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +13 -0
  94. package/dist/index.js.map +1 -1
  95. package/dist/keyboard.d.ts +39 -2
  96. package/dist/keyboard.d.ts.map +1 -1
  97. package/dist/keyboard.js +145 -62
  98. package/dist/keyboard.js.map +1 -1
  99. package/dist/layout-store.d.ts +26 -0
  100. package/dist/layout-store.d.ts.map +1 -0
  101. package/dist/layout-store.js +46 -0
  102. package/dist/layout-store.js.map +1 -0
  103. package/dist/panel-levels.d.ts +18 -0
  104. package/dist/panel-levels.d.ts.map +1 -0
  105. package/dist/panel-levels.js +41 -0
  106. package/dist/panel-levels.js.map +1 -0
  107. package/dist/panels.d.ts +28 -0
  108. package/dist/panels.d.ts.map +1 -0
  109. package/dist/panels.js +73 -0
  110. package/dist/panels.js.map +1 -0
  111. package/dist/prefs.d.ts +5 -2
  112. package/dist/prefs.d.ts.map +1 -1
  113. package/dist/prefs.js +12 -3
  114. package/dist/prefs.js.map +1 -1
  115. package/dist/react.d.ts +460 -0
  116. package/dist/react.d.ts.map +1 -0
  117. package/dist/react.js +2 -0
  118. package/dist/react.js.map +1 -0
  119. package/dist/root-element.d.ts +122 -0
  120. package/dist/root-element.d.ts.map +1 -0
  121. package/dist/root-element.js +541 -0
  122. package/dist/root-element.js.map +1 -0
  123. package/dist/root.d.ts +30 -30
  124. package/dist/root.d.ts.map +1 -1
  125. package/dist/root.js +65 -42
  126. package/dist/root.js.map +1 -1
  127. package/dist/session.d.ts +19 -3
  128. package/dist/session.d.ts.map +1 -1
  129. package/dist/session.js +15 -17
  130. package/dist/session.js.map +1 -1
  131. package/dist/standalone/kgdiff-viewer.js +4 -4
  132. package/dist/styles.d.ts +4 -0
  133. package/dist/styles.d.ts.map +1 -1
  134. package/dist/styles.js +224 -84
  135. package/dist/styles.js.map +1 -1
  136. package/dist/theme.d.ts +7 -0
  137. package/dist/theme.d.ts.map +1 -1
  138. package/dist/theme.js +10 -7
  139. package/dist/theme.js.map +1 -1
  140. package/dist/topics.d.ts +8 -0
  141. package/dist/topics.d.ts.map +1 -0
  142. package/dist/topics.js +18 -0
  143. package/dist/topics.js.map +1 -0
  144. package/dist/ui/change-list.d.ts.map +1 -1
  145. package/dist/ui/change-list.js +2 -2
  146. package/dist/ui/change-list.js.map +1 -1
  147. package/dist/ui/context.d.ts +20 -7
  148. package/dist/ui/context.d.ts.map +1 -1
  149. package/dist/ui/context.js.map +1 -1
  150. package/dist/ui/header.d.ts +41 -3
  151. package/dist/ui/header.d.ts.map +1 -1
  152. package/dist/ui/header.js +99 -34
  153. package/dist/ui/header.js.map +1 -1
  154. package/dist/ui/legend.d.ts +15 -0
  155. package/dist/ui/legend.d.ts.map +1 -0
  156. package/dist/ui/legend.js +68 -0
  157. package/dist/ui/legend.js.map +1 -0
  158. package/dist/ui/popovers.d.ts +3 -1
  159. package/dist/ui/popovers.d.ts.map +1 -1
  160. package/dist/ui/popovers.js +62 -23
  161. package/dist/ui/popovers.js.map +1 -1
  162. package/dist/ui/search.d.ts.map +1 -1
  163. package/dist/ui/search.js +3 -2
  164. package/dist/ui/search.js.map +1 -1
  165. package/dist/ui/sidebar.d.ts +26 -3
  166. package/dist/ui/sidebar.d.ts.map +1 -1
  167. package/dist/ui/sidebar.js +344 -12
  168. package/dist/ui/sidebar.js.map +1 -1
  169. package/dist/ui/stage.d.ts +56 -1
  170. package/dist/ui/stage.d.ts.map +1 -1
  171. package/dist/ui/stage.js +244 -256
  172. package/dist/ui/stage.js.map +1 -1
  173. package/dist/ui/transport.d.ts +12 -0
  174. package/dist/ui/transport.d.ts.map +1 -0
  175. package/dist/ui/transport.js +198 -0
  176. package/dist/ui/transport.js.map +1 -0
  177. package/dist/ui/zoom.d.ts +15 -0
  178. package/dist/ui/zoom.d.ts.map +1 -0
  179. package/dist/ui/zoom.js +30 -0
  180. package/dist/ui/zoom.js.map +1 -0
  181. package/dist/view.d.ts +94 -0
  182. package/dist/view.d.ts.map +1 -0
  183. package/dist/view.js +441 -0
  184. package/dist/view.js.map +1 -0
  185. package/package.json +95 -12
  186. package/src/bind.ts +80 -0
  187. package/src/controller.ts +51 -442
  188. package/src/dom.ts +30 -0
  189. package/src/element.ts +364 -406
  190. package/src/elements/actions.ts +94 -0
  191. package/src/elements/bound.ts +247 -0
  192. package/src/elements/change-detail.ts +34 -0
  193. package/src/elements/change-list.ts +51 -0
  194. package/src/elements/filters.ts +34 -0
  195. package/src/elements/legend.ts +103 -0
  196. package/src/elements/minimap.ts +75 -0
  197. package/src/elements/root.ts +86 -0
  198. package/src/elements/search.ts +38 -0
  199. package/src/elements/stage-context.ts +34 -0
  200. package/src/elements/stage.ts +285 -0
  201. package/src/elements/title.ts +34 -0
  202. package/src/elements/transport.ts +39 -0
  203. package/src/elements/view-bound.ts +118 -0
  204. package/src/elements/view-dock.ts +43 -0
  205. package/src/elements/zoom.ts +38 -0
  206. package/src/i18n.ts +20 -0
  207. package/src/icons.ts +1 -0
  208. package/src/index.ts +347 -34
  209. package/src/keyboard.ts +188 -61
  210. package/src/layout-store.ts +71 -0
  211. package/src/panel-levels.ts +53 -0
  212. package/src/panels.ts +81 -0
  213. package/src/prefs.ts +12 -4
  214. package/src/react.ts +459 -0
  215. package/src/root-element.ts +624 -0
  216. package/src/root.ts +92 -65
  217. package/src/session.ts +22 -33
  218. package/src/styles.ts +245 -84
  219. package/src/theme.ts +10 -7
  220. package/src/topics.ts +37 -0
  221. package/src/ui/change-list.ts +6 -2
  222. package/src/ui/context.ts +20 -7
  223. package/src/ui/header.ts +127 -56
  224. package/src/ui/legend.ts +112 -0
  225. package/src/ui/popovers.ts +68 -25
  226. package/src/ui/search.ts +10 -5
  227. package/src/ui/sidebar.ts +386 -23
  228. package/src/ui/stage.ts +267 -331
  229. package/src/ui/transport.ts +240 -0
  230. package/src/ui/zoom.ts +45 -0
  231. package/src/view.ts +516 -0
  232. 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
- * In phase 1 the root lives inside `<kgdiff-viewer>` (the element is its host) and the regions
8
- * are the default layout's; phase 2 makes it `<kgdiff-root>` and the regions elements of their
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 { createContext, provideContext, requestContext } from './context.js';
13
- import { type ControllerHost, ViewerController } from './controller.js';
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, UiContext } from './ui/context.js';
24
+ import type { FocusTarget, UiActions } from './ui/context.js';
24
25
 
25
- /** Something the root renders: a region, or the layout itself. */
26
- export interface Region {
27
- update(topics: ReadonlySet<Topic>): void;
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 a root provides to the regions that bind to it. */
31
- export interface ViewerContext {
32
- readonly controller: ViewerController;
33
- readonly actions: UiActions;
34
- /** The element the root's events are dispatched on and its tokens are set on. */
35
- readonly host: HTMLElement;
36
- /** Render `region` on every notification (in binding order); returns the detach. */
37
- attach(region: Region): () => void;
38
- target(name: FocusTarget, focus: () => void): () => void;
39
- focus(name: FocusTarget): boolean;
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
- export const viewerContext = createContext<ViewerContext>('kgdiff.viewer/1');
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: ViewerController;
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, (() => void)[]>();
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: ControllerHost = {
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
- this.controller = new ViewerController(events, {
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
- list.push(focus);
169
+ const target: Target = { focus, available: available ?? (() => true) };
170
+ list.push(target);
146
171
  return () => {
147
- const i = list.indexOf(focus);
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 focus = this.#targets.get(name)?.at(-1);
154
- focus?.();
155
- return focus !== undefined;
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[1] !== tokens) {
189
- this.#shadow.adoptedStyleSheets = [stylesheet(doc), tokens];
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
- | 'status'
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 {