@kgdiff/viewer 0.4.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 (257) 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/context.d.ts +42 -0
  8. package/dist/context.d.ts.map +1 -0
  9. package/dist/context.js +87 -0
  10. package/dist/context.js.map +1 -0
  11. package/dist/controller.d.ts +26 -237
  12. package/dist/controller.d.ts.map +1 -1
  13. package/dist/controller.js +33 -1484
  14. package/dist/controller.js.map +1 -1
  15. package/dist/dom.d.ts +10 -0
  16. package/dist/dom.d.ts.map +1 -1
  17. package/dist/dom.js +29 -0
  18. package/dist/dom.js.map +1 -1
  19. package/dist/element.d.ts +51 -42
  20. package/dist/element.d.ts.map +1 -1
  21. package/dist/element.js +381 -602
  22. package/dist/element.js.map +1 -1
  23. package/dist/elements/actions.d.ts +27 -0
  24. package/dist/elements/actions.d.ts.map +1 -0
  25. package/dist/elements/actions.js +81 -0
  26. package/dist/elements/actions.js.map +1 -0
  27. package/dist/elements/bound.d.ts +63 -0
  28. package/dist/elements/bound.d.ts.map +1 -0
  29. package/dist/elements/bound.js +213 -0
  30. package/dist/elements/bound.js.map +1 -0
  31. package/dist/elements/change-detail.d.ts +25 -0
  32. package/dist/elements/change-detail.d.ts.map +1 -0
  33. package/dist/elements/change-detail.js +19 -0
  34. package/dist/elements/change-detail.js.map +1 -0
  35. package/dist/elements/change-list.d.ts +27 -0
  36. package/dist/elements/change-list.d.ts.map +1 -0
  37. package/dist/elements/change-list.js +29 -0
  38. package/dist/elements/change-list.js.map +1 -0
  39. package/dist/elements/filters.d.ts +25 -0
  40. package/dist/elements/filters.d.ts.map +1 -0
  41. package/dist/elements/filters.js +19 -0
  42. package/dist/elements/filters.js.map +1 -0
  43. package/dist/elements/legend.d.ts +40 -0
  44. package/dist/elements/legend.d.ts.map +1 -0
  45. package/dist/elements/legend.js +87 -0
  46. package/dist/elements/legend.js.map +1 -0
  47. package/dist/elements/minimap.d.ts +38 -0
  48. package/dist/elements/minimap.d.ts.map +1 -0
  49. package/dist/elements/minimap.js +59 -0
  50. package/dist/elements/minimap.js.map +1 -0
  51. package/dist/elements/root.d.ts +40 -0
  52. package/dist/elements/root.d.ts.map +1 -0
  53. package/dist/elements/root.js +67 -0
  54. package/dist/elements/root.js.map +1 -0
  55. package/dist/elements/search.d.ts +29 -0
  56. package/dist/elements/search.d.ts.map +1 -0
  57. package/dist/elements/search.js +23 -0
  58. package/dist/elements/search.js.map +1 -0
  59. package/dist/elements/stage-context.d.ts +31 -0
  60. package/dist/elements/stage-context.d.ts.map +1 -0
  61. package/dist/elements/stage-context.js +4 -0
  62. package/dist/elements/stage-context.js.map +1 -0
  63. package/dist/elements/stage.d.ts +65 -0
  64. package/dist/elements/stage.d.ts.map +1 -0
  65. package/dist/elements/stage.js +259 -0
  66. package/dist/elements/stage.js.map +1 -0
  67. package/dist/elements/title.d.ts +25 -0
  68. package/dist/elements/title.d.ts.map +1 -0
  69. package/dist/elements/title.js +19 -0
  70. package/dist/elements/title.js.map +1 -0
  71. package/dist/elements/transport.d.ts +29 -0
  72. package/dist/elements/transport.d.ts.map +1 -0
  73. package/dist/elements/transport.js +24 -0
  74. package/dist/elements/transport.js.map +1 -0
  75. package/dist/elements/view-bound.d.ts +39 -0
  76. package/dist/elements/view-bound.d.ts.map +1 -0
  77. package/dist/elements/view-bound.js +88 -0
  78. package/dist/elements/view-bound.js.map +1 -0
  79. package/dist/elements/view-dock.d.ts +28 -0
  80. package/dist/elements/view-dock.d.ts.map +1 -0
  81. package/dist/elements/view-dock.js +28 -0
  82. package/dist/elements/view-dock.js.map +1 -0
  83. package/dist/elements/zoom.d.ts +28 -0
  84. package/dist/elements/zoom.d.ts.map +1 -0
  85. package/dist/elements/zoom.js +22 -0
  86. package/dist/elements/zoom.js.map +1 -0
  87. package/dist/i18n.d.ts +10 -0
  88. package/dist/i18n.d.ts.map +1 -1
  89. package/dist/i18n.js +20 -0
  90. package/dist/i18n.js.map +1 -1
  91. package/dist/icons.d.ts +1 -0
  92. package/dist/icons.d.ts.map +1 -1
  93. package/dist/icons.js +1 -0
  94. package/dist/icons.js.map +1 -1
  95. package/dist/index.d.ts +331 -36
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +13 -0
  98. package/dist/index.js.map +1 -1
  99. package/dist/keyboard.d.ts +39 -2
  100. package/dist/keyboard.d.ts.map +1 -1
  101. package/dist/keyboard.js +145 -62
  102. package/dist/keyboard.js.map +1 -1
  103. package/dist/layout-store.d.ts +26 -0
  104. package/dist/layout-store.d.ts.map +1 -0
  105. package/dist/layout-store.js +46 -0
  106. package/dist/layout-store.js.map +1 -0
  107. package/dist/panel-levels.d.ts +18 -0
  108. package/dist/panel-levels.d.ts.map +1 -0
  109. package/dist/panel-levels.js +41 -0
  110. package/dist/panel-levels.js.map +1 -0
  111. package/dist/panels.d.ts +28 -0
  112. package/dist/panels.d.ts.map +1 -0
  113. package/dist/panels.js +73 -0
  114. package/dist/panels.js.map +1 -0
  115. package/dist/pipeline.d.ts.map +1 -1
  116. package/dist/pipeline.js +3 -2
  117. package/dist/pipeline.js.map +1 -1
  118. package/dist/prefs.d.ts +5 -2
  119. package/dist/prefs.d.ts.map +1 -1
  120. package/dist/prefs.js +12 -3
  121. package/dist/prefs.js.map +1 -1
  122. package/dist/react.d.ts +460 -0
  123. package/dist/react.d.ts.map +1 -0
  124. package/dist/react.js +2 -0
  125. package/dist/react.js.map +1 -0
  126. package/dist/root-element.d.ts +122 -0
  127. package/dist/root-element.d.ts.map +1 -0
  128. package/dist/root-element.js +541 -0
  129. package/dist/root-element.js.map +1 -0
  130. package/dist/root.d.ts +54 -0
  131. package/dist/root.d.ts.map +1 -0
  132. package/dist/root.js +288 -0
  133. package/dist/root.js.map +1 -0
  134. package/dist/schedule.d.ts +22 -0
  135. package/dist/schedule.d.ts.map +1 -0
  136. package/dist/schedule.js +41 -0
  137. package/dist/schedule.js.map +1 -0
  138. package/dist/session.d.ts +269 -0
  139. package/dist/session.d.ts.map +1 -0
  140. package/dist/session.js +1237 -0
  141. package/dist/session.js.map +1 -0
  142. package/dist/standalone/kgdiff-viewer-compute.js +1 -1
  143. package/dist/standalone/kgdiff-viewer.js +4 -4
  144. package/dist/styles.d.ts +17 -6
  145. package/dist/styles.d.ts.map +1 -1
  146. package/dist/styles.js +367 -258
  147. package/dist/styles.js.map +1 -1
  148. package/dist/theme.d.ts +32 -0
  149. package/dist/theme.d.ts.map +1 -0
  150. package/dist/theme.js +152 -0
  151. package/dist/theme.js.map +1 -0
  152. package/dist/topics.d.ts +8 -0
  153. package/dist/topics.d.ts.map +1 -0
  154. package/dist/topics.js +18 -0
  155. package/dist/topics.js.map +1 -0
  156. package/dist/ui/change-list.d.ts +18 -0
  157. package/dist/ui/change-list.d.ts.map +1 -0
  158. package/dist/ui/change-list.js +331 -0
  159. package/dist/ui/change-list.js.map +1 -0
  160. package/dist/ui/context.d.ts +34 -7
  161. package/dist/ui/context.d.ts.map +1 -1
  162. package/dist/ui/context.js.map +1 -1
  163. package/dist/ui/filters.d.ts +9 -0
  164. package/dist/ui/filters.d.ts.map +1 -0
  165. package/dist/ui/filters.js +68 -0
  166. package/dist/ui/filters.js.map +1 -0
  167. package/dist/ui/header.d.ts +41 -3
  168. package/dist/ui/header.d.ts.map +1 -1
  169. package/dist/ui/header.js +101 -36
  170. package/dist/ui/header.js.map +1 -1
  171. package/dist/ui/legend.d.ts +15 -0
  172. package/dist/ui/legend.d.ts.map +1 -0
  173. package/dist/ui/legend.js +68 -0
  174. package/dist/ui/legend.js.map +1 -0
  175. package/dist/ui/popovers.d.ts +3 -1
  176. package/dist/ui/popovers.d.ts.map +1 -1
  177. package/dist/ui/popovers.js +62 -23
  178. package/dist/ui/popovers.js.map +1 -1
  179. package/dist/ui/search.d.ts +13 -0
  180. package/dist/ui/search.d.ts.map +1 -0
  181. package/dist/ui/search.js +192 -0
  182. package/dist/ui/search.js.map +1 -0
  183. package/dist/ui/sidebar.d.ts +32 -17
  184. package/dist/ui/sidebar.d.ts.map +1 -1
  185. package/dist/ui/sidebar.js +327 -519
  186. package/dist/ui/sidebar.js.map +1 -1
  187. package/dist/ui/stage.d.ts +56 -1
  188. package/dist/ui/stage.d.ts.map +1 -1
  189. package/dist/ui/stage.js +244 -256
  190. package/dist/ui/stage.js.map +1 -1
  191. package/dist/ui/swatch.js +1 -1
  192. package/dist/ui/swatch.js.map +1 -1
  193. package/dist/ui/transport.d.ts +12 -0
  194. package/dist/ui/transport.d.ts.map +1 -0
  195. package/dist/ui/transport.js +198 -0
  196. package/dist/ui/transport.js.map +1 -0
  197. package/dist/ui/zoom.d.ts +15 -0
  198. package/dist/ui/zoom.d.ts.map +1 -0
  199. package/dist/ui/zoom.js +30 -0
  200. package/dist/ui/zoom.js.map +1 -0
  201. package/dist/view.d.ts +94 -0
  202. package/dist/view.d.ts.map +1 -0
  203. package/dist/view.js +441 -0
  204. package/dist/view.js.map +1 -0
  205. package/package.json +95 -12
  206. package/src/bind.ts +80 -0
  207. package/src/context.ts +106 -0
  208. package/src/controller.ts +52 -1630
  209. package/src/dom.ts +30 -0
  210. package/src/element.ts +386 -619
  211. package/src/elements/actions.ts +94 -0
  212. package/src/elements/bound.ts +247 -0
  213. package/src/elements/change-detail.ts +34 -0
  214. package/src/elements/change-list.ts +51 -0
  215. package/src/elements/filters.ts +34 -0
  216. package/src/elements/legend.ts +103 -0
  217. package/src/elements/minimap.ts +75 -0
  218. package/src/elements/root.ts +86 -0
  219. package/src/elements/search.ts +38 -0
  220. package/src/elements/stage-context.ts +34 -0
  221. package/src/elements/stage.ts +285 -0
  222. package/src/elements/title.ts +34 -0
  223. package/src/elements/transport.ts +39 -0
  224. package/src/elements/view-bound.ts +118 -0
  225. package/src/elements/view-dock.ts +43 -0
  226. package/src/elements/zoom.ts +38 -0
  227. package/src/i18n.ts +20 -0
  228. package/src/icons.ts +1 -0
  229. package/src/index.ts +350 -35
  230. package/src/keyboard.ts +188 -61
  231. package/src/layout-store.ts +71 -0
  232. package/src/panel-levels.ts +53 -0
  233. package/src/panels.ts +81 -0
  234. package/src/pipeline.ts +4 -1
  235. package/src/prefs.ts +12 -4
  236. package/src/react.ts +459 -0
  237. package/src/root-element.ts +624 -0
  238. package/src/root.ts +335 -0
  239. package/src/schedule.ts +43 -0
  240. package/src/session.ts +1375 -0
  241. package/src/styles.ts +396 -263
  242. package/src/theme.ts +170 -0
  243. package/src/topics.ts +37 -0
  244. package/src/ui/change-list.ts +405 -0
  245. package/src/ui/context.ts +36 -7
  246. package/src/ui/filters.ts +79 -0
  247. package/src/ui/header.ts +129 -58
  248. package/src/ui/legend.ts +112 -0
  249. package/src/ui/popovers.ts +68 -25
  250. package/src/ui/search.ts +225 -0
  251. package/src/ui/sidebar.ts +362 -639
  252. package/src/ui/stage.ts +267 -331
  253. package/src/ui/swatch.ts +1 -1
  254. package/src/ui/transport.ts +240 -0
  255. package/src/ui/zoom.ts +45 -0
  256. package/src/view.ts +516 -0
  257. package/vscode.html-custom-data.json +726 -0
package/src/ui/swatch.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  import type { Family } from '@kgdiff/scene';
6
6
 
7
7
  export function swatch(family: Family): SVGSVGElement {
8
- const v = (name: string) => `var(--kg-${name})`;
8
+ const v = (name: string) => `var(--kgdiff-${name})`;
9
9
  const c = family === 'neutral' ? v('neutral') : v(family);
10
10
  let body: string;
11
11
  switch (family) {
@@ -0,0 +1,240 @@
1
+ /**
2
+ * The replay transport (RFC 0001 §3, `<kgdiff-transport>`): previous / play-pause / next, the
3
+ * scrubber with one tick per change (colored by family, the change under the pointer named in a
4
+ * tip), the position and the speed; and the "Replay finished · Restart" pill shown above it at
5
+ * the end. Shown in replay mode only. The stage stacks it above the view dock.
6
+ */
7
+ import type { ChangeFamily } from '@kgdiff/scene';
8
+ import {
9
+ attr,
10
+ h,
11
+ icon,
12
+ iconButton,
13
+ relabel,
14
+ replace,
15
+ replaceKeepingFocus,
16
+ setText,
17
+ } from '../dom.js';
18
+ import type { Translator } from '../i18n.js';
19
+ import type { Topic } from '../topics.js';
20
+ import { sceneHunkGlyph, type UiContext } from './context.js';
21
+
22
+ const TICK_W = 1000;
23
+
24
+ export class Transport {
25
+ /** The toolbar. */
26
+ readonly el: HTMLElement;
27
+ /** The pill shown at the end of a replay (it takes the caption's place). */
28
+ readonly finished: HTMLElement;
29
+ readonly #play: HTMLButtonElement;
30
+ readonly #prev: HTMLButtonElement;
31
+ readonly #next: HTMLButtonElement;
32
+ readonly #scrubber: HTMLElement;
33
+ readonly #fill: HTMLElement;
34
+ readonly #knob: HTMLElement;
35
+ readonly #ticks: SVGSVGElement;
36
+ readonly #tip: HTMLElement;
37
+ readonly #pos: HTMLElement;
38
+ readonly #speed: HTMLButtonElement;
39
+ #scrubbing: number | null = null;
40
+
41
+ readonly #ctx: UiContext;
42
+
43
+ constructor(ctx: UiContext) {
44
+ this.#ctx = ctx;
45
+ const c = ctx.controller;
46
+ this.#prev = iconButton('skip-back', '', () => c.stepReplay(-1), { class: 'icon-btn' });
47
+ this.#play = iconButton('play', '', () => c.togglePlay(), { class: 'icon-btn play' });
48
+ this.#next = iconButton('skip-forward', '', () => c.stepReplay(1), { class: 'icon-btn' });
49
+ this.#fill = h('div', { class: 'fill' });
50
+ this.#knob = h('div', { class: 'knob' });
51
+ this.#ticks = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
52
+ this.#ticks.setAttribute('class', 'ticks');
53
+ this.#ticks.setAttribute('viewBox', `0 0 ${TICK_W} 14`);
54
+ this.#ticks.setAttribute('preserveAspectRatio', 'none');
55
+ this.#ticks.setAttribute('aria-hidden', 'true');
56
+ this.#tip = h('div', { class: 'scrub-tip card', hidden: true });
57
+ this.#scrubber = h(
58
+ 'div',
59
+ { class: 'scrubber', role: 'slider', tabindex: '0', 'aria-valuemin': '0' },
60
+ h('div', { class: 'track' }, this.#fill),
61
+ this.#ticks,
62
+ this.#knob,
63
+ this.#tip,
64
+ );
65
+ this.#bindScrubber();
66
+ this.#pos = h('span', { class: 'pos', 'aria-hidden': 'true' });
67
+ this.#speed = h('button', { type: 'button', class: 'speed', onclick: () => c.cycleSpeed() });
68
+ this.el = h(
69
+ 'div',
70
+ { class: 'transport card', role: 'toolbar' },
71
+ this.#prev,
72
+ this.#play,
73
+ this.#next,
74
+ this.#scrubber,
75
+ this.#pos,
76
+ this.#speed,
77
+ );
78
+ // Not a status: the root's live region announces the end, once however many transports.
79
+ this.finished = h('div', { class: 'finished card', hidden: true });
80
+ }
81
+
82
+ update(topics: ReadonlySet<Topic>): void {
83
+ const tr = this.#ctx.tr();
84
+ const all = topics.has('status') || topics.has('data') || topics.has('locale');
85
+ if (all) this.#renderLabels(tr);
86
+ if (all || topics.has('theme')) this.#renderTicks();
87
+ if (all || topics.has('mode')) this.el.hidden = this.#ctx.controller.mode !== 'replay';
88
+ if (all || topics.has('mode') || topics.has('replay')) this.#render(tr);
89
+ }
90
+
91
+ #renderLabels(tr: Translator): void {
92
+ relabel(this.#prev, `${tr.t('detail.prev')} (←)`);
93
+ relabel(this.#next, `${tr.t('detail.next')} (→)`);
94
+ attr(this.el, 'aria-label', tr.t('replay.label'));
95
+ attr(this.#scrubber, 'aria-label', tr.t('replay.scrubber'));
96
+ }
97
+
98
+ #render(tr: Translator): void {
99
+ const c = this.#ctx.controller;
100
+ const clock = c.clock;
101
+ if (c.mode !== 'replay' || !clock || !c.scene) {
102
+ this.finished.hidden = true;
103
+ return;
104
+ }
105
+ const p = clock.progress;
106
+ this.#fill.style.transform = `scaleX(${p})`;
107
+ this.#knob.style.left = `${p * 100}%`;
108
+ const position = clock.position();
109
+ const total = clock.total;
110
+ setText(this.#pos, tr.t('replay.position', { i: position, n: total }));
111
+ attr(this.#scrubber, 'aria-valuemax', String(Math.round(clock.duration)));
112
+ attr(this.#scrubber, 'aria-valuenow', String(Math.round(clock.t)));
113
+ attr(this.#scrubber, 'aria-valuetext', tr.t('replay.position', { i: position, n: total }));
114
+ const playing = clock.playing;
115
+ if (this.#play.dataset.state !== String(playing)) {
116
+ this.#play.dataset.state = String(playing);
117
+ replace(this.#play, icon(playing ? 'pause' : 'play'));
118
+ }
119
+ relabel(this.#play, `${tr.t(playing ? 'replay.pause' : 'replay.play')} (Space)`);
120
+ setText(this.#speed, `${clock.speed}×`);
121
+ relabel(this.#speed, tr.t('replay.speed', { s: `${clock.speed}×` }));
122
+ this.#prev.disabled = clock.t <= 0;
123
+ this.#next.disabled = clock.t >= clock.duration;
124
+ const finished = clock.finished;
125
+ this.finished.hidden = !finished;
126
+ if (finished && this.finished.childElementCount === 0) {
127
+ replace(
128
+ this.finished,
129
+ icon('check-circle'),
130
+ h('span', { text: tr.t('replay.finished') }),
131
+ h(
132
+ 'button',
133
+ { type: 'button', class: 'btn', onclick: () => c.restart() },
134
+ icon('restart'),
135
+ tr.t('replay.restart'),
136
+ ),
137
+ );
138
+ } else if (!finished && this.finished.childElementCount > 0) {
139
+ // Restart was pressed from the pill: keep focus in the transport.
140
+ replaceKeepingFocus(this.#ctx.root, this.finished, this.#play);
141
+ }
142
+ }
143
+
144
+ #renderTicks(): void {
145
+ const c = this.#ctx.controller;
146
+ const clock = c.clock;
147
+ const scene = c.scene;
148
+ while (this.#ticks.firstChild) this.#ticks.removeChild(this.#ticks.firstChild);
149
+ if (!clock || !scene || clock.duration <= 0) return;
150
+ const paths = new Map<ChangeFamily, string[]>();
151
+ for (const w of clock.timeline.windows) {
152
+ const fam = scene.hunks[w.hunk]?.family;
153
+ if (!fam) continue;
154
+ const x = ((w.start / clock.duration) * TICK_W).toFixed(1);
155
+ let list = paths.get(fam);
156
+ if (!list) {
157
+ list = [];
158
+ paths.set(fam, list);
159
+ }
160
+ list.push(`M${x} 2V12`);
161
+ }
162
+ for (const [fam, d] of paths) {
163
+ const p = document.createElementNS('http://www.w3.org/2000/svg', 'path');
164
+ p.setAttribute('d', d.join(''));
165
+ p.setAttribute('stroke', `var(--kgdiff-${fam})`);
166
+ p.setAttribute('stroke-width', '2');
167
+ p.setAttribute('stroke-linecap', 'round');
168
+ p.setAttribute('vector-effect', 'non-scaling-stroke');
169
+ this.#ticks.appendChild(p);
170
+ }
171
+ }
172
+
173
+ #bindScrubber(): void {
174
+ const timeAt = (clientX: number): number => {
175
+ const clock = this.#ctx.controller.clock;
176
+ const r = this.#scrubber.getBoundingClientRect();
177
+ if (!clock || r.width <= 0) return 0;
178
+ return Math.max(0, Math.min(1, (clientX - r.left) / r.width)) * clock.duration;
179
+ };
180
+ this.#scrubber.addEventListener('pointerdown', (e) => {
181
+ this.#scrubbing = e.pointerId;
182
+ this.#scrubber.setPointerCapture?.(e.pointerId);
183
+ this.#ctx.controller.pause();
184
+ this.#ctx.controller.seek(timeAt(e.clientX));
185
+ });
186
+ this.#scrubber.addEventListener('pointermove', (e) => {
187
+ if (this.#scrubbing === e.pointerId) this.#ctx.controller.seek(timeAt(e.clientX));
188
+ this.#showTip(e.clientX, timeAt(e.clientX));
189
+ });
190
+ const end = (e: PointerEvent): void => {
191
+ if (this.#scrubbing === e.pointerId) this.#scrubbing = null;
192
+ };
193
+ this.#scrubber.addEventListener('pointerup', end);
194
+ this.#scrubber.addEventListener('pointercancel', end);
195
+ this.#scrubber.addEventListener('pointerleave', () => {
196
+ this.#tip.hidden = true;
197
+ });
198
+ this.#scrubber.addEventListener('keydown', (e) => {
199
+ const c = this.#ctx.controller;
200
+ if (e.key === 'Home') c.seek(0);
201
+ else if (e.key === 'End') c.seek(c.clock?.duration ?? 0);
202
+ else return;
203
+ e.preventDefault();
204
+ e.stopPropagation();
205
+ });
206
+ }
207
+
208
+ #showTip(clientX: number, t: number): void {
209
+ const c = this.#ctx.controller;
210
+ const clock = c.clock;
211
+ const scene = c.scene;
212
+ if (!clock || !scene) return;
213
+ // Nearest window start: the tick under the pointer.
214
+ let best = -1;
215
+ let bestD = Number.POSITIVE_INFINITY;
216
+ for (const w of clock.timeline.windows) {
217
+ const d = Math.abs(w.start - t);
218
+ if (d < bestD) {
219
+ bestD = d;
220
+ best = w.hunk;
221
+ }
222
+ }
223
+ const hunk = scene.hunks[best];
224
+ if (!hunk) {
225
+ this.#tip.hidden = true;
226
+ return;
227
+ }
228
+ const r = this.#scrubber.getBoundingClientRect();
229
+ this.#tip.hidden = false;
230
+ this.#tip.style.left = `${clientX - r.left}px`;
231
+ if (this.#tip.dataset.hunk !== hunk.id) {
232
+ this.#tip.dataset.hunk = hunk.id;
233
+ replace(
234
+ this.#tip,
235
+ h('span', { class: 'glyph-box', 'data-family': hunk.family, text: sceneHunkGlyph(hunk) }),
236
+ h('span', { text: hunk.title }),
237
+ );
238
+ }
239
+ }
240
+ }
package/src/ui/zoom.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The zoom buttons (RFC 0001 §3, `<kgdiff-zoom>`): zoom in, zoom out and fit, acting on one view
3
+ * (a stage's camera), not on every view of the session as the `+` / `−` / `f` keys do.
4
+ */
5
+ import { attr, h, iconButton, relabel } from '../dom.js';
6
+ import type { Topic } from '../topics.js';
7
+ import type { UiContext } from './context.js';
8
+
9
+ /** What the buttons act on: a view (`StageView`). */
10
+ export interface ZoomTarget {
11
+ zoom(direction: 1 | -1): void;
12
+ fit(): Promise<void>;
13
+ }
14
+
15
+ export class ZoomBar {
16
+ readonly el: HTMLElement;
17
+ readonly #in: HTMLButtonElement;
18
+ readonly #out: HTMLButtonElement;
19
+ readonly #fit: HTMLButtonElement;
20
+ readonly #ctx: UiContext;
21
+
22
+ /** `target`: the view the buttons act on now (null: none yet, the buttons do nothing). */
23
+ constructor(ctx: UiContext, target: () => ZoomTarget | null) {
24
+ this.#ctx = ctx;
25
+ this.#in = iconButton('plus', '', () => target()?.zoom(1));
26
+ this.#out = iconButton('minus', '', () => target()?.zoom(-1));
27
+ this.#fit = iconButton('fit', '', () => void target()?.fit());
28
+ this.el = h(
29
+ 'div',
30
+ { class: 'zoom-bar card', role: 'toolbar', 'aria-orientation': 'vertical' },
31
+ this.#in,
32
+ this.#out,
33
+ this.#fit,
34
+ );
35
+ }
36
+
37
+ update(topics: ReadonlySet<Topic>): void {
38
+ if (!(topics.has('status') || topics.has('data') || topics.has('locale'))) return;
39
+ const tr = this.#ctx.tr();
40
+ attr(this.el, 'aria-label', tr.t('zoom.label'));
41
+ relabel(this.#in, `${tr.t('action.zoomIn')} (+)`);
42
+ relabel(this.#out, `${tr.t('action.zoomOut')} (−)`);
43
+ relabel(this.#fit, `${tr.t('action.fit')} (F)`);
44
+ }
45
+ }