@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
|
@@ -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
|
+
}
|