@kgdiff/viewer 0.3.0 → 0.4.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 (73) hide show
  1. package/README.md +9 -8
  2. package/dist/controller.d.ts +23 -7
  3. package/dist/controller.d.ts.map +1 -1
  4. package/dist/controller.js +119 -14
  5. package/dist/controller.js.map +1 -1
  6. package/dist/element.d.ts +6 -3
  7. package/dist/element.d.ts.map +1 -1
  8. package/dist/element.js +68 -19
  9. package/dist/element.js.map +1 -1
  10. package/dist/hash-sync.d.ts +4 -2
  11. package/dist/hash-sync.d.ts.map +1 -1
  12. package/dist/hash-sync.js +21 -5
  13. package/dist/hash-sync.js.map +1 -1
  14. package/dist/i18n.d.ts +7 -7
  15. package/dist/i18n.d.ts.map +1 -1
  16. package/dist/i18n.js +14 -14
  17. package/dist/i18n.js.map +1 -1
  18. package/dist/index.d.ts +21 -8
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/keyboard.d.ts +6 -4
  22. package/dist/keyboard.d.ts.map +1 -1
  23. package/dist/keyboard.js +6 -5
  24. package/dist/keyboard.js.map +1 -1
  25. package/dist/prefs.d.ts +7 -1
  26. package/dist/prefs.d.ts.map +1 -1
  27. package/dist/prefs.js +15 -3
  28. package/dist/prefs.js.map +1 -1
  29. package/dist/standalone/kgdiff-viewer.js +4 -4
  30. package/dist/styles.d.ts.map +1 -1
  31. package/dist/styles.js +69 -63
  32. package/dist/styles.js.map +1 -1
  33. package/dist/ui/context.d.ts +2 -0
  34. package/dist/ui/context.d.ts.map +1 -1
  35. package/dist/ui/context.js.map +1 -1
  36. package/dist/ui/dock.d.ts +17 -0
  37. package/dist/ui/dock.d.ts.map +1 -0
  38. package/dist/ui/dock.js +151 -0
  39. package/dist/ui/dock.js.map +1 -0
  40. package/dist/ui/header.d.ts +7 -2
  41. package/dist/ui/header.d.ts.map +1 -1
  42. package/dist/ui/header.js +24 -133
  43. package/dist/ui/header.js.map +1 -1
  44. package/dist/ui/popovers.d.ts.map +1 -1
  45. package/dist/ui/popovers.js +3 -2
  46. package/dist/ui/popovers.js.map +1 -1
  47. package/dist/ui/sidebar.d.ts.map +1 -1
  48. package/dist/ui/sidebar.js +2 -1
  49. package/dist/ui/sidebar.js.map +1 -1
  50. package/dist/ui/stage.d.ts +4 -0
  51. package/dist/ui/stage.d.ts.map +1 -1
  52. package/dist/ui/stage.js +41 -94
  53. package/dist/ui/stage.js.map +1 -1
  54. package/dist/ui/swatch.d.ts +7 -0
  55. package/dist/ui/swatch.d.ts.map +1 -0
  56. package/dist/ui/swatch.js +26 -0
  57. package/dist/ui/swatch.js.map +1 -0
  58. package/package.json +6 -6
  59. package/src/controller.ts +131 -20
  60. package/src/element.ts +72 -21
  61. package/src/hash-sync.ts +20 -6
  62. package/src/i18n.ts +15 -14
  63. package/src/index.ts +21 -9
  64. package/src/keyboard.ts +12 -6
  65. package/src/prefs.ts +19 -4
  66. package/src/styles.ts +69 -63
  67. package/src/ui/context.ts +2 -0
  68. package/src/ui/dock.ts +198 -0
  69. package/src/ui/header.ts +38 -168
  70. package/src/ui/popovers.ts +3 -2
  71. package/src/ui/sidebar.ts +2 -1
  72. package/src/ui/stage.ts +49 -114
  73. package/src/ui/swatch.ts +31 -0
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @kgdiff/viewer
2
2
 
3
- `<kgdiff-viewer>`, a framework-agnostic web component for reviewing graph diffs. Four views
4
- (Overlay, Swipe, Replay and Changes only), a searchable change list, a detail panel with
3
+ `<kgdiff-viewer>`, a framework-agnostic web component for reviewing graph diffs. Three ways to
4
+ compare (Overlay, Swipe, Replay), each with or without Changes only, a searchable change list, a detail panel with
5
5
  character-level diffs, review progress, deep links, keyboard shortcuts, light and dark themes, and
6
6
  an English and Chinese UI.
7
7
 
@@ -37,9 +37,10 @@ ESM only. Runs in evergreen browsers.
37
37
  self-contained script that defines the element and shows embedded payloads. The optional
38
38
  `@kgdiff/viewer/standalone/compute` add-on brings the diff engine and force layout for `load()`.
39
39
 
40
- Attributes: `mode` (`overlay` | `swipe` | `replay` | `changes`), `theme`, `palette`, `locale`,
41
- `sidebar`, `sync-hash`, `global-shortcuts`, `header`, `chrome`, `legend`, `loop`, `loop-delay`,
42
- `pause-on-hover`. Methods: `load`, `selectHunk`, `next`, `prev`, `play`, `pause`, `fit`,
40
+ Attributes: `mode` (`overlay` | `swipe` | `replay`), `scope` (`all` | `changes`), `theme`,
41
+ `palette`, `locale`, `sidebar`, `sync-hash`, `global-shortcuts`, `header`, `chrome`, `legend`,
42
+ `loop`, `loop-delay`, `pause-on-hover`. The legacy `mode="changes"` still works: it means
43
+ `overlay` with `scope="changes"`. Methods: `load`, `selectHunk`, `next`, `prev`, `play`, `pause`, `fit`,
43
44
  `exportDelta`, `reviewState`, `setReviewed`. Events: `kgdiff-ready`, `kgdiff-select`,
44
45
  `kgdiff-mode`, `kgdiff-review`, `kgdiff-error`, `kgdiff-replay-end`.
45
46
 
@@ -60,9 +61,9 @@ replay loop:
60
61
  | `loop-delay` (`loopDelay`) | ms between the end and restart | `1200` |
61
62
  | `pause-on-hover` (`pauseOnHover`) | `false` to keep playing | on: a looping replay pauses under a mouse or pen |
62
63
 
63
- - `chrome="none"` shows the graph alone: no header (view switch, theme, full screen, help,
64
- sidebar button), change list, step caption, playback bar, finished card, zoom buttons, minimap
65
- or legend card. The hover tooltip stays, and the legend becomes one line with the families this
64
+ - `chrome="none"` shows the graph alone: no title, actions (theme, help, sidebar button), view
65
+ dock (with its step caption, playback bar and finished card), change list, zoom buttons,
66
+ minimap or legend card. The hover tooltip stays, and the legend becomes one line with the families this
66
67
  diff has, then unchanged. Clicks and keyboard shortcuts do nothing. The minimum height drops
67
68
  from 420px to 120px. The view stays fitted: when the canvas is resized (unless the user
68
69
  panned or zoomed since) and at the start of every loop. Labels are never cut at the edge: one
@@ -2,8 +2,9 @@
2
2
  * Viewer state and orchestration, independent of the UI:
3
3
  *
4
4
  * - load pipeline with progress steps (sources or a precomputed report);
5
- * - mode, selection (with an animated spotlight `emphasis`), family filters, review progress;
6
- * - changes-only focus with expandable collapsed groups (`extendLayout` + scene rebuild);
5
+ * - view (mode × scope), selection (with an animated spotlight `emphasis`), family filters,
6
+ * review progress;
7
+ * - the `changes` scope's focus with expandable collapsed groups (`extendLayout` + scene rebuild);
7
8
  * - replay clock driven by one requestAnimationFrame loop that sleeps when idle, with a camera
8
9
  * that follows the playing hunk / chapter unless the user moved it in the last 2 s;
9
10
  * - swipe position, hover, theme / palette / locale (rebuilding the scene when they change).
@@ -15,18 +16,19 @@ import type { LayoutResult } from '@kgdiff/layout';
15
16
  import { type Camera } from '@kgdiff/renderer';
16
17
  import { type ChangeFamily, type FocusState, type PaletteName, type Scene, type Theme, type ThemeName, type VisualResolver } from '@kgdiff/scene';
17
18
  import { type ChromeLayout } from './framing.js';
18
- import type { LoadOptions, ReportPayload, ReviewState, ThemePreference, ViewerMode } from './index.js';
19
+ import type { LoadOptions, ReportPayload, ReviewState, ThemePreference, ViewerMode, ViewerScope } from './index.js';
19
20
  import { type ListModel } from './list-model.js';
20
21
  import { type LoadedDiff, type LoadStep } from './pipeline.js';
22
+ import { LEGACY_CHANGES_MODE } from './prefs.js';
21
23
  import { ReplayClock } from './replay.js';
22
24
  import { type StorageLike } from './review-store.js';
23
25
  import { type SearchHit } from './search.js';
24
26
  export type Status = 'idle' | 'loading' | 'ready' | 'error';
25
- export type Topic = 'status' | 'data' | 'mode' | 'selection' | 'filters' | 'review' | 'replay' | 'hover' | 'theme' | 'locale' | 'focus' | 'swipe';
27
+ export type Topic = 'status' | 'data' | 'mode' | 'selection' | 'filters' | 'review' | 'replay' | 'hover' | 'theme' | 'locale' | 'focus' | 'swipe' | 'overview';
26
28
  export interface ControllerHost {
27
29
  ready(stats: DiffStats): void;
28
30
  select(hunk: string | null, node: string | null): void;
29
- mode(mode: ViewerMode): void;
31
+ mode(mode: ViewerMode, scope: ViewerScope): void;
30
32
  review(hunk: string, reviewed: boolean, progress: ReviewState): void;
31
33
  error(error: Error): void;
32
34
  announce(text: string): void;
@@ -46,7 +48,7 @@ export interface ControllerDeps {
46
48
  readonly yieldToPaint: () => Promise<void>;
47
49
  readonly storage: StorageLike | null;
48
50
  }
49
- export type Preference = 'mode' | 'theme' | 'palette' | 'locale';
51
+ export type Preference = 'mode' | 'scope' | 'theme' | 'palette' | 'locale';
50
52
  /** Camera follow is suppressed this long after the user moved the camera (ms). */
51
53
  export declare const USER_CAMERA_GRACE = 2000;
52
54
  /** The camera follows when less than this share of the target bounds is in view. */
@@ -55,6 +57,8 @@ export declare const FOLLOW_COVERAGE = 0.7;
55
57
  export declare const FLY_MAX_ZOOM = 1.8;
56
58
  /** Fitting everything never zooms in further (the renderer's default). */
57
59
  export declare const FIT_MAX_ZOOM = 2.5;
60
+ /** Everything counts as in view (the minimap hides) from this share of the content bounds on. */
61
+ export declare const OVERVIEW_COVERAGE = 0.98;
58
62
  /** Share of `bounds` inside `view`, in [0, 1]. */
59
63
  export declare function coverage(bounds: Rect, view: Rect): number;
60
64
  export declare class ViewerController {
@@ -70,6 +74,7 @@ export declare class ViewerController {
70
74
  resolver: VisualResolver | null;
71
75
  list: ListModel;
72
76
  mode: ViewerMode;
77
+ scope: ViewerScope;
73
78
  themePreference: ThemePreference;
74
79
  palette: PaletteName;
75
80
  locale: Locale;
@@ -98,6 +103,8 @@ export declare class ViewerController {
98
103
  autoFit: boolean;
99
104
  /** Canvas clicks select (and double clicks fly); off for embeds without controls. */
100
105
  selectOnClick: boolean;
106
+ /** The whole content is in view (the minimap has nothing to add). */
107
+ overview: boolean;
101
108
  constructor(host: ControllerHost, deps: ControllerDeps);
102
109
  subscribe(listener: (topics: ReadonlySet<Topic>) => void): () => void;
103
110
  /** Deliver pending notifications now (also used by tests). */
@@ -138,7 +145,16 @@ export declare class ViewerController {
138
145
  invalidate(): void;
139
146
  /** Resolve the current view and hand it to the renderer (and the minimap). */
140
147
  draw(now?: number): void;
141
- setMode(value: ViewerMode): void;
148
+ /** Whether the scope is fixed to `changes` (the layout covers only the changes). */
149
+ get scopeLocked(): boolean;
150
+ /** Switch how the versions are compared. The legacy `changes` means overlay + `changes` scope. */
151
+ setMode(value: ViewerMode | typeof LEGACY_CHANGES_MODE): void;
152
+ /** Show the whole graph or only the changes, in any mode. */
153
+ setScope(value: ViewerScope): void;
154
+ /** Toggle the `changes` scope (key `4`); says why when the scope is locked. */
155
+ toggleScope(): void;
156
+ /** Set mode and scope together (one notification, one `kgdiff-mode` event). */
157
+ setView(mode: ViewerMode, scope: ViewerScope): void;
142
158
  selectHunkById(id: string | null, options?: {
143
159
  fly?: boolean;
144
160
  }): void;
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"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"}
@@ -6,7 +6,7 @@ import { chromeInsets, FRAME_PADDING, freeView, NO_INSETS, zoomFit, } from './fr
6
6
  import { translate } from './i18n.js';
7
7
  import { buildList, EMPTY_LIST, stepHunk } from './list-model.js';
8
8
  import { readPayload, runLoad } from './pipeline.js';
9
- import { toLocale, toMode, toPalette, toTheme } from './prefs.js';
9
+ import { LEGACY_CHANGES_MODE, toLocale, toMode, toPalette, toScope, toTheme } from './prefs.js';
10
10
  import { renderBackend } from './renderer-port.js';
11
11
  import { ReplayClock } from './replay.js';
12
12
  import { ReviewStore, reviewKey } from './review-store.js';
@@ -23,6 +23,8 @@ export const FLY_MAX_ZOOM = 1.8;
23
23
  export const FIT_MAX_ZOOM = 2.5;
24
24
  /** Minimap marks are re-rasterized at most this often while replaying (ms). */
25
25
  const MINIMAP_INTERVAL = 200;
26
+ /** Everything counts as in view (the minimap hides) from this share of the content bounds on. */
27
+ export const OVERVIEW_COVERAGE = 0.98;
26
28
  class Superseded extends Error {
27
29
  }
28
30
  /** Share of `bounds` inside `view`, in [0, 1]. */
@@ -52,6 +54,7 @@ export class ViewerController {
52
54
  resolver = null;
53
55
  list = EMPTY_LIST;
54
56
  mode = 'overlay';
57
+ scope = 'all';
55
58
  themePreference = 'auto';
56
59
  palette = 'default';
57
60
  locale = 'en';
@@ -81,6 +84,8 @@ export class ViewerController {
81
84
  /** Canvas clicks select (and double clicks fly); off for embeds without controls. */
82
85
  selectOnClick = true;
83
86
  #labelsInside = false;
87
+ /** The whole content is in view (the minimap has nothing to add). */
88
+ overview = true;
84
89
  #review = null;
85
90
  #searchIndex = null;
86
91
  #renderer = null;
@@ -98,6 +103,7 @@ export class ViewerController {
98
103
  #followed = '';
99
104
  #loadToken = 0;
100
105
  #pendingHunk = null;
106
+ #boundsCache = null;
101
107
  #chrome = null;
102
108
  /** Camera kept across detach / attach (element moved in the DOM). */
103
109
  #savedCamera = null;
@@ -270,10 +276,16 @@ export class ViewerController {
270
276
  this.locale = view.locale;
271
277
  }
272
278
  const previousMode = this.mode;
273
- if (!this.explicit.has('mode')) {
279
+ const previousScope = this.scope;
280
+ if (!this.explicit.has('mode'))
281
+ this.mode = view?.mode ?? 'overlay';
282
+ if (layout.scope === 'changes') {
283
+ // Only the changes are laid out: there is no whole graph to show.
284
+ this.scope = 'changes';
285
+ }
286
+ else if (!this.explicit.has('scope')) {
274
287
  const threshold = loaded.layoutOptions?.largeGraphThreshold ?? 3000;
275
- const large = layout.scope === 'changes' || diff.nodes.length > threshold;
276
- this.mode = view?.mode ?? (large ? 'changes' : 'overlay');
288
+ this.scope = view?.scope ?? (diff.nodes.length > threshold ? 'changes' : 'all');
277
289
  }
278
290
  this.#review = new ReviewStore(this.#deps.storage, reviewKey(diff.delta.base.hash, diff.delta.target.hash), diff.hunks.map((h) => h.id));
279
291
  this.#rebuildScene();
@@ -283,15 +295,16 @@ export class ViewerController {
283
295
  // Learn early whether out-of-layout groups can be expanded (standalone reports).
284
296
  loadCompute().then(() => this.#notify('focus'), () => this.#notify('focus'));
285
297
  }
286
- this.#notify('status', 'data', 'mode', 'selection', 'filters', 'review', 'replay', 'theme', 'locale', 'focus', 'swipe');
298
+ this.#notify('status', 'data', 'mode', 'selection', 'filters', 'review', 'replay', 'theme', 'locale', 'focus', 'swipe', 'overview');
287
299
  // A new diff starts fitted, after the UI has shown its chrome (the notification above is
288
300
  // delivered first) and once the canvas has a size (see `layoutChanged`).
289
301
  this.#needsFit = true;
290
302
  this.#savedCamera = null;
291
303
  queueMicrotask(() => this.layoutChanged());
292
304
  this.#host.ready(diff.stats);
293
- if (this.mode !== previousMode)
294
- this.#host.mode(this.mode);
305
+ if (this.mode !== previousMode || this.scope !== previousScope) {
306
+ this.#host.mode(this.mode, this.scope);
307
+ }
295
308
  const pending = this.#pendingHunk;
296
309
  this.#pendingHunk = null;
297
310
  if (pending !== null)
@@ -334,7 +347,7 @@ export class ViewerController {
334
347
  }
335
348
  #updateFocus() {
336
349
  const scene = this.scene;
337
- if (!scene || this.mode !== 'changes') {
350
+ if (!scene || this.scope !== 'changes') {
338
351
  this.focus = null;
339
352
  return;
340
353
  }
@@ -364,6 +377,7 @@ export class ViewerController {
364
377
  this.#unbind.push(renderer.on('hover', (pick) => this.#onHover(pick)), renderer.on('click', ({ pick }) => this.#onCanvasClick(pick)), renderer.on('dblclick', ({ pick }) => this.#onCanvasDblClick(pick)), renderer.on('camera', ({ source }) => {
365
378
  if (source === 'user')
366
379
  this.noteUserCamera();
380
+ this.#updateOverview();
367
381
  }));
368
382
  this.#minimapDirty = true;
369
383
  this.invalidate();
@@ -410,6 +424,7 @@ export class ViewerController {
410
424
  * `autoFit`) fit again unless the user moved the camera since the last fit.
411
425
  */
412
426
  layoutChanged() {
427
+ this.#updateOverview();
413
428
  if (this.status !== 'ready')
414
429
  return;
415
430
  if (!this.#needsFit) {
@@ -570,27 +585,70 @@ export class ViewerController {
570
585
  duration: this.theme.motion.base,
571
586
  };
572
587
  }
573
- // ─── Mode ─────────────────────────────────────────────────────────────────
588
+ // ─── View: mode × scope ───────────────────────────────────────────────────
589
+ /** Whether the scope is fixed to `changes` (the layout covers only the changes). */
590
+ get scopeLocked() {
591
+ return this.layout?.scope === 'changes';
592
+ }
593
+ /** Switch how the versions are compared. The legacy `changes` means overlay + `changes` scope. */
574
594
  setMode(value) {
595
+ if (value === LEGACY_CHANGES_MODE) {
596
+ this.setView('overlay', 'changes');
597
+ return;
598
+ }
575
599
  const mode = toMode(value);
576
- if (mode === null || mode === this.mode)
600
+ if (mode !== null)
601
+ this.setView(mode, this.scope);
602
+ }
603
+ /** Show the whole graph or only the changes, in any mode. */
604
+ setScope(value) {
605
+ const scope = toScope(value);
606
+ if (scope !== null)
607
+ this.setView(this.mode, scope);
608
+ }
609
+ /** Toggle the `changes` scope (key `4`); says why when the scope is locked. */
610
+ toggleScope() {
611
+ if (this.scopeLocked) {
612
+ this.#host.announce(translate(this.locale, 'scope.locked'));
613
+ return;
614
+ }
615
+ this.setScope(this.scope === 'changes' ? 'all' : 'changes');
616
+ }
617
+ /** Set mode and scope together (one notification, one `kgdiff-mode` event). */
618
+ setView(mode, scope) {
619
+ const nextScope = this.scopeLocked ? 'changes' : scope;
620
+ const modeChanged = mode !== this.mode;
621
+ const scopeChanged = nextScope !== this.scope;
622
+ if (!modeChanged && !scopeChanged)
577
623
  return;
578
624
  this.mode = mode;
625
+ this.scope = nextScope;
579
626
  if (mode !== 'replay') {
580
627
  this.clock?.pause();
581
628
  this.#restartAt = Number.NaN;
582
629
  this.#resume = false;
583
630
  }
584
631
  this.#followed = '';
585
- this.#updateFocus();
632
+ if (scopeChanged) {
633
+ this.#updateFocus();
634
+ this.#updateOverview();
635
+ }
586
636
  this.#minimapDirty = true;
587
637
  this.#notify('mode', 'focus', 'replay');
588
638
  this.invalidate();
589
639
  if (this.status === 'ready') {
590
- this.#host.mode(mode);
591
- this.#host.announce(translate(this.locale, 'announce.mode', { mode: translate(this.locale, `mode.${mode}`) }));
640
+ this.#host.mode(mode, nextScope);
641
+ const parts = [];
642
+ if (modeChanged) {
643
+ parts.push(translate(this.locale, 'announce.mode', { mode: translate(this.locale, `mode.${mode}`) }));
644
+ }
645
+ if (scopeChanged) {
646
+ parts.push(translate(this.locale, nextScope === 'changes' ? 'scope.on' : 'scope.off'));
647
+ }
648
+ this.#host.announce(parts.join(' · '));
592
649
  }
593
- if (mode === 'replay')
650
+ // Switching to replay starts a looping replay; toggling the scope leaves playback alone.
651
+ if (modeChanged && mode === 'replay')
594
652
  this.#autoplay();
595
653
  }
596
654
  // ─── Selection ────────────────────────────────────────────────────────────
@@ -905,6 +963,29 @@ export class ViewerController {
905
963
  }
906
964
  return { bounds: fit.bounds, insets };
907
965
  }
966
+ /** Bounds of what is drawn: the scene, or the visible part of the `changes` scope's focus. */
967
+ #contentBounds() {
968
+ const scene = this.scene;
969
+ if (!scene)
970
+ return null;
971
+ const focus = this.focus;
972
+ const cached = this.#boundsCache;
973
+ if (cached && cached.scene === scene && cached.focus === focus)
974
+ return cached.rect;
975
+ const rect = focus ? focusBounds(scene, focus) : frameBounds(scene);
976
+ this.#boundsCache = { scene, focus, rect };
977
+ return rect;
978
+ }
979
+ /** Whether everything drawn is in view; the minimap hides while it is. */
980
+ #updateOverview() {
981
+ const bounds = this.#contentBounds();
982
+ const view = this.#renderer?.camera.viewport();
983
+ const next = !bounds || !view || coverage(bounds, view) >= OVERVIEW_COVERAGE;
984
+ if (next === this.overview)
985
+ return;
986
+ this.overview = next;
987
+ this.#notify('overview');
988
+ }
908
989
  zoom(direction) {
909
990
  this.noteUserCamera();
910
991
  this.#renderer?.camera.zoomBy(direction > 0 ? 1.4 : 1 / 1.4);
@@ -1012,6 +1093,7 @@ export class ViewerController {
1012
1093
  return;
1013
1094
  this.hops = h;
1014
1095
  this.#updateFocus();
1096
+ this.#updateOverview();
1015
1097
  this.#minimapDirty = true;
1016
1098
  this.#notify('focus');
1017
1099
  this.invalidate();
@@ -1141,6 +1223,7 @@ export class ViewerController {
1141
1223
  }
1142
1224
  #focusChanged() {
1143
1225
  this.#updateFocus();
1226
+ this.#updateOverview();
1144
1227
  this.#minimapDirty = true;
1145
1228
  this.#notify('focus');
1146
1229
  this.invalidate();
@@ -1394,6 +1477,28 @@ function isString(x) {
1394
1477
  function sameInsets(a, b) {
1395
1478
  return a.top === b.top && a.right === b.right && a.bottom === b.bottom && a.left === b.left;
1396
1479
  }
1480
+ /** Bounds of the visible part of `focus`, with room for labels and the collapsed pills. */
1481
+ function focusBounds(scene, focus) {
1482
+ let x0 = Number.POSITIVE_INFINITY;
1483
+ let y0 = Number.POSITIVE_INFINITY;
1484
+ let x1 = Number.NEGATIVE_INFINITY;
1485
+ let y1 = Number.NEGATIVE_INFINITY;
1486
+ const add = (x, y, rx, top, bottom) => {
1487
+ x0 = Math.min(x0, x - rx);
1488
+ x1 = Math.max(x1, x + rx);
1489
+ y0 = Math.min(y0, y - top);
1490
+ y1 = Math.max(y1, y + bottom);
1491
+ };
1492
+ // Labels sit centered below their node: leave room for a typical one.
1493
+ for (const n of scene.nodes)
1494
+ if (focus.nodes[n.index] === 1)
1495
+ add(n.x, n.y, n.r + 48, n.r, n.r + 24);
1496
+ for (const g of focus.collapsed)
1497
+ add(g.x, g.y, 48, 12, 12);
1498
+ if (!(x0 <= x1))
1499
+ return frameBounds(scene);
1500
+ return padRect({ x: x0, y: y0, width: x1 - x0, height: y1 - y0 }, 160, 1.12);
1501
+ }
1397
1502
  /** Pad a small rect so a node is shown with its neighbourhood. */
1398
1503
  function padRect(r, min = 200, ratio = 1.4) {
1399
1504
  const w = Math.max(min, r.width * ratio);