@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.
- package/README.md +9 -8
- package/dist/controller.d.ts +23 -7
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +119 -14
- package/dist/controller.js.map +1 -1
- package/dist/element.d.ts +6 -3
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +68 -19
- package/dist/element.js.map +1 -1
- package/dist/hash-sync.d.ts +4 -2
- package/dist/hash-sync.d.ts.map +1 -1
- package/dist/hash-sync.js +21 -5
- package/dist/hash-sync.js.map +1 -1
- package/dist/i18n.d.ts +7 -7
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +14 -14
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +21 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keyboard.d.ts +6 -4
- package/dist/keyboard.d.ts.map +1 -1
- package/dist/keyboard.js +6 -5
- package/dist/keyboard.js.map +1 -1
- package/dist/prefs.d.ts +7 -1
- package/dist/prefs.d.ts.map +1 -1
- package/dist/prefs.js +15 -3
- package/dist/prefs.js.map +1 -1
- package/dist/standalone/kgdiff-viewer.js +4 -4
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +69 -63
- package/dist/styles.js.map +1 -1
- package/dist/ui/context.d.ts +2 -0
- package/dist/ui/context.d.ts.map +1 -1
- package/dist/ui/context.js.map +1 -1
- package/dist/ui/dock.d.ts +17 -0
- package/dist/ui/dock.d.ts.map +1 -0
- package/dist/ui/dock.js +151 -0
- package/dist/ui/dock.js.map +1 -0
- package/dist/ui/header.d.ts +7 -2
- package/dist/ui/header.d.ts.map +1 -1
- package/dist/ui/header.js +24 -133
- package/dist/ui/header.js.map +1 -1
- package/dist/ui/popovers.d.ts.map +1 -1
- package/dist/ui/popovers.js +3 -2
- package/dist/ui/popovers.js.map +1 -1
- package/dist/ui/sidebar.d.ts.map +1 -1
- package/dist/ui/sidebar.js +2 -1
- package/dist/ui/sidebar.js.map +1 -1
- package/dist/ui/stage.d.ts +4 -0
- package/dist/ui/stage.d.ts.map +1 -1
- package/dist/ui/stage.js +41 -94
- package/dist/ui/stage.js.map +1 -1
- package/dist/ui/swatch.d.ts +7 -0
- package/dist/ui/swatch.d.ts.map +1 -0
- package/dist/ui/swatch.js +26 -0
- package/dist/ui/swatch.js.map +1 -0
- package/package.json +6 -6
- package/src/controller.ts +131 -20
- package/src/element.ts +72 -21
- package/src/hash-sync.ts +20 -6
- package/src/i18n.ts +15 -14
- package/src/index.ts +21 -9
- package/src/keyboard.ts +12 -6
- package/src/prefs.ts +19 -4
- package/src/styles.ts +69 -63
- package/src/ui/context.ts +2 -0
- package/src/ui/dock.ts +198 -0
- package/src/ui/header.ts +38 -168
- package/src/ui/popovers.ts +3 -2
- package/src/ui/sidebar.ts +2 -1
- package/src/ui/stage.ts +49 -114
- package/src/ui/swatch.ts +31 -0
package/src/ui/stage.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The canvas area and its floating chrome: replay caption and transport
|
|
3
|
-
*
|
|
4
|
-
* zoom controls + minimap
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* The canvas area and its floating chrome: the view dock (with replay's caption and transport
|
|
3
|
+
* stacked above it), swipe divider, legend (a card, or one line of dots for chrome-less embeds),
|
|
4
|
+
* zoom controls + minimap (shown only while part of the content is out of view), hover tooltip
|
|
5
|
+
* and the loading / identical / error / empty states. Overlays never cover the current hunk:
|
|
6
|
+
* fits and flights frame it in the free part of the canvas.
|
|
7
7
|
*/
|
|
8
8
|
import { KgdiffError, type Rect } from '@kgdiff/core';
|
|
9
9
|
import type { ChangeFamily, Family } from '@kgdiff/scene';
|
|
@@ -22,39 +22,14 @@ import type { ChromeLayout } from '../framing.js';
|
|
|
22
22
|
import type { Translator } from '../i18n.js';
|
|
23
23
|
import { FAMILIES } from '../styles.js';
|
|
24
24
|
import { sceneHunkGlyph, type UiContext } from './context.js';
|
|
25
|
+
import { Dock } from './dock.js';
|
|
26
|
+
import { swatch } from './swatch.js';
|
|
25
27
|
|
|
26
28
|
const TICK_W = 1000;
|
|
27
29
|
|
|
28
30
|
/** How the legend shows: the card, one line of dots, or not at all. */
|
|
29
31
|
export type LegendStyle = 'full' | 'mini' | 'none';
|
|
30
32
|
|
|
31
|
-
/** Legend swatch: the node cue of each family (docs/visual-language.md §Meaning families). */
|
|
32
|
-
function swatch(family: Family): SVGSVGElement {
|
|
33
|
-
const v = (name: string) => `var(--kg-${name})`;
|
|
34
|
-
const c = family === 'neutral' ? v('neutral') : v(family);
|
|
35
|
-
let body: string;
|
|
36
|
-
switch (family) {
|
|
37
|
-
case 'added':
|
|
38
|
-
body = `<circle cx="11" cy="11" r="8.5" fill="none" stroke="${c}" stroke-opacity=".35" stroke-width="2"/><circle cx="11" cy="11" r="5" fill="${c}"/>`;
|
|
39
|
-
break;
|
|
40
|
-
case 'removed':
|
|
41
|
-
body = `<circle cx="11" cy="11" r="5.25" fill="none" stroke="${c}" stroke-width="1.5" stroke-dasharray="3 2.5"/>`;
|
|
42
|
-
break;
|
|
43
|
-
case 'identity':
|
|
44
|
-
body = `<circle cx="11" cy="11" r="9.5" fill="none" stroke="${c}" stroke-opacity=".3" stroke-width="1.5"/><circle cx="11" cy="11" r="7.25" fill="none" stroke="${c}" stroke-width="1.5"/><circle cx="11" cy="11" r="4.25" fill="${c}"/>`;
|
|
45
|
-
break;
|
|
46
|
-
case 'content':
|
|
47
|
-
case 'structure':
|
|
48
|
-
body = `<circle cx="11" cy="11" r="7.75" fill="none" stroke="${c}" stroke-width="1.5"/><circle cx="11" cy="11" r="4.5" fill="${c}"/>`;
|
|
49
|
-
break;
|
|
50
|
-
default:
|
|
51
|
-
body = `<circle cx="11" cy="11" r="3.5" fill="${c}"/>`;
|
|
52
|
-
}
|
|
53
|
-
const tpl = document.createElement('template');
|
|
54
|
-
tpl.innerHTML = `<svg class="swatch" viewBox="0 0 22 22" aria-hidden="true" focusable="false">${body}</svg>`;
|
|
55
|
-
return tpl.content.firstElementChild as SVGSVGElement;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
33
|
export class Stage {
|
|
59
34
|
readonly el: HTMLElement;
|
|
60
35
|
readonly canvasHost: HTMLElement;
|
|
@@ -80,10 +55,9 @@ export class Stage {
|
|
|
80
55
|
readonly #divider: HTMLElement;
|
|
81
56
|
readonly #chipBefore: HTMLElement;
|
|
82
57
|
readonly #chipAfter: HTMLElement;
|
|
83
|
-
//
|
|
84
|
-
readonly
|
|
85
|
-
readonly #
|
|
86
|
-
readonly #expandButton: HTMLButtonElement;
|
|
58
|
+
// Dock
|
|
59
|
+
readonly dock: Dock;
|
|
60
|
+
readonly #dockWrap: HTMLElement;
|
|
87
61
|
// Chrome
|
|
88
62
|
readonly #legend: HTMLElement;
|
|
89
63
|
readonly #legendHead: HTMLButtonElement;
|
|
@@ -93,12 +67,13 @@ export class Stage {
|
|
|
93
67
|
readonly #zoomOut: HTMLButtonElement;
|
|
94
68
|
readonly #fitButton: HTMLButtonElement;
|
|
95
69
|
readonly #mapCluster: HTMLElement;
|
|
70
|
+
readonly #zoomBar: HTMLElement;
|
|
96
71
|
readonly tooltip: HTMLElement;
|
|
97
72
|
readonly #state: HTMLElement;
|
|
98
73
|
#pointer: { x: number; y: number } | null = null;
|
|
99
74
|
#scrubbing: number | null = null;
|
|
100
75
|
#dragging: number | null = null;
|
|
101
|
-
#legendOpen
|
|
76
|
+
#legendOpen = false;
|
|
102
77
|
#stateKey = '';
|
|
103
78
|
|
|
104
79
|
readonly #ctx: UiContext;
|
|
@@ -187,39 +162,15 @@ export class Stage {
|
|
|
187
162
|
this.#divider,
|
|
188
163
|
);
|
|
189
164
|
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const b = h('button', {
|
|
194
|
-
type: 'button',
|
|
195
|
-
class: 'opt',
|
|
196
|
-
role: 'radio',
|
|
197
|
-
text: String(n),
|
|
198
|
-
onclick: () => c.setHops(n),
|
|
199
|
-
});
|
|
200
|
-
this.#hopButtons.push(b);
|
|
201
|
-
opts.appendChild(b);
|
|
202
|
-
}
|
|
203
|
-
opts.addEventListener('keydown', (e) => {
|
|
204
|
-
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
|
|
205
|
-
e.preventDefault();
|
|
206
|
-
e.stopPropagation();
|
|
207
|
-
const next = Math.max(0, Math.min(2, c.hops + (e.key === 'ArrowRight' ? 1 : -1)));
|
|
208
|
-
c.setHops(next);
|
|
209
|
-
this.#hopButtons[next]?.focus();
|
|
210
|
-
});
|
|
211
|
-
this.#expandButton = h(
|
|
212
|
-
'button',
|
|
213
|
-
{ type: 'button', class: 'expand', onclick: () => c.expandNearest() },
|
|
214
|
-
icon('plus'),
|
|
215
|
-
h('span', { class: 'n' }),
|
|
216
|
-
);
|
|
217
|
-
this.#stepper = h(
|
|
165
|
+
// View dock, with replay's finished pill, caption and transport stacked above it.
|
|
166
|
+
this.dock = new Dock(ctx);
|
|
167
|
+
this.#dockWrap = h(
|
|
218
168
|
'div',
|
|
219
|
-
{ class: '
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
this.#
|
|
169
|
+
{ class: 'dock' },
|
|
170
|
+
this.#finished,
|
|
171
|
+
this.#caption,
|
|
172
|
+
this.#transport,
|
|
173
|
+
this.dock.el,
|
|
223
174
|
);
|
|
224
175
|
|
|
225
176
|
// Legend
|
|
@@ -228,7 +179,8 @@ export class Stage {
|
|
|
228
179
|
{
|
|
229
180
|
type: 'button',
|
|
230
181
|
class: 'legend-head',
|
|
231
|
-
|
|
182
|
+
'aria-expanded': 'false',
|
|
183
|
+
onclick: () => this.#setLegend(!this.#legendOpen),
|
|
232
184
|
},
|
|
233
185
|
h('span', { class: 'lbl' }),
|
|
234
186
|
icon('chevron-down'),
|
|
@@ -237,7 +189,7 @@ export class Stage {
|
|
|
237
189
|
attr(this.#legendHead, 'aria-controls', 'kg-legend');
|
|
238
190
|
this.#legend = h(
|
|
239
191
|
'div',
|
|
240
|
-
{ class: 'legend card float bottom-left', 'data-open': '
|
|
192
|
+
{ class: 'legend card float bottom-left', 'data-open': 'false' },
|
|
241
193
|
this.#legendHead,
|
|
242
194
|
this.#legendBody,
|
|
243
195
|
);
|
|
@@ -248,16 +200,17 @@ export class Stage {
|
|
|
248
200
|
this.#zoomOut = iconButton('minus', '', () => c.zoom(-1));
|
|
249
201
|
this.#fitButton = iconButton('fit', '', () => void c.fit());
|
|
250
202
|
this.minimapHost = h('div', { class: 'minimap-host' });
|
|
203
|
+
this.#zoomBar = h(
|
|
204
|
+
'div',
|
|
205
|
+
{ class: 'zoom-bar card', role: 'toolbar' },
|
|
206
|
+
this.#zoomIn,
|
|
207
|
+
this.#zoomOut,
|
|
208
|
+
this.#fitButton,
|
|
209
|
+
);
|
|
251
210
|
this.#mapCluster = h(
|
|
252
211
|
'div',
|
|
253
212
|
{ class: 'map-cluster float bottom-right' },
|
|
254
|
-
|
|
255
|
-
'div',
|
|
256
|
-
{ class: 'zoom-bar card', role: 'toolbar' },
|
|
257
|
-
this.#zoomIn,
|
|
258
|
-
this.#zoomOut,
|
|
259
|
-
this.#fitButton,
|
|
260
|
-
),
|
|
213
|
+
this.#zoomBar,
|
|
261
214
|
this.minimapHost,
|
|
262
215
|
);
|
|
263
216
|
|
|
@@ -267,13 +220,10 @@ export class Stage {
|
|
|
267
220
|
'div',
|
|
268
221
|
{ class: 'chrome' },
|
|
269
222
|
this.#swipe,
|
|
270
|
-
this.#caption,
|
|
271
|
-
this.#stepper,
|
|
272
223
|
this.#legend,
|
|
273
224
|
this.#miniLegend,
|
|
274
225
|
this.#mapCluster,
|
|
275
|
-
this.#
|
|
276
|
-
this.#transport,
|
|
226
|
+
this.#dockWrap,
|
|
277
227
|
this.tooltip,
|
|
278
228
|
);
|
|
279
229
|
this.el = h(
|
|
@@ -305,16 +255,19 @@ export class Stage {
|
|
|
305
255
|
const parts = [
|
|
306
256
|
this.#legend,
|
|
307
257
|
this.#miniLegend,
|
|
308
|
-
this.#
|
|
258
|
+
this.#zoomBar,
|
|
259
|
+
this.dock.el,
|
|
309
260
|
this.#transport,
|
|
310
261
|
this.#finished,
|
|
311
|
-
this.#stepper,
|
|
312
262
|
this.#chipBefore,
|
|
313
263
|
this.#chipAfter,
|
|
314
264
|
];
|
|
315
265
|
if (this.#caption.dataset.empty !== 'true') parts.push(this.#caption);
|
|
266
|
+
if (this.minimapHost.dataset.hidden !== 'true') parts.push(this.minimapHost);
|
|
267
|
+
// The HUD (title, actions) floats over the canvas too.
|
|
268
|
+
for (const el of this.el.querySelectorAll<HTMLElement>('.hud .card')) parts.push(el);
|
|
316
269
|
for (const el of parts) {
|
|
317
|
-
if (el.hidden) continue;
|
|
270
|
+
if (el.hidden || el.closest('[hidden]')) continue;
|
|
318
271
|
const b = el.getBoundingClientRect();
|
|
319
272
|
if (b.width <= 0 || b.height <= 0) continue;
|
|
320
273
|
obstacles.push({ x: b.left - r.left, y: b.top - r.top, width: b.width, height: b.height });
|
|
@@ -322,10 +275,9 @@ export class Stage {
|
|
|
322
275
|
return { width: r.width, height: r.height, obstacles };
|
|
323
276
|
}
|
|
324
277
|
|
|
325
|
-
/**
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
return hgt === 0 || hgt >= 560;
|
|
278
|
+
/** Re-measure what depends on rendered sizes (fonts, resizes). */
|
|
279
|
+
layout(): void {
|
|
280
|
+
this.dock.layout();
|
|
329
281
|
}
|
|
330
282
|
|
|
331
283
|
#setLegend(open: boolean): void {
|
|
@@ -350,10 +302,11 @@ export class Stage {
|
|
|
350
302
|
this.#renderState(tr);
|
|
351
303
|
this.#renderTicks();
|
|
352
304
|
this.#renderCanvasLabel(tr);
|
|
353
|
-
if (this.#legendOpen === null) this.#setLegend(this.#defaultLegendOpen());
|
|
354
305
|
}
|
|
355
|
-
|
|
356
|
-
|
|
306
|
+
this.dock.update(topics);
|
|
307
|
+
if (all || topics.has('mode')) this.#renderMode();
|
|
308
|
+
if (all || topics.has('overview')) {
|
|
309
|
+
attr(this.minimapHost, 'data-hidden', String(c.overview));
|
|
357
310
|
}
|
|
358
311
|
if (all || topics.has('mode') || topics.has('replay')) this.#renderReplay(tr);
|
|
359
312
|
if (all || topics.has('mode') || topics.has('swipe')) this.#renderSwipe();
|
|
@@ -373,8 +326,6 @@ export class Stage {
|
|
|
373
326
|
setText(this.#chipBefore, tr.t('detail.before'));
|
|
374
327
|
setText(this.#chipAfter, tr.t('detail.after'));
|
|
375
328
|
attr(this.#divider, 'aria-label', tr.t('swipe.divider'));
|
|
376
|
-
setText(this.#stepper.querySelector('.label') as Element, tr.t('focus.context'));
|
|
377
|
-
attr(this.#stepper.querySelector('.opts') as Element, 'aria-label', tr.t('focus.hops'));
|
|
378
329
|
setText(this.#legendHead.querySelector('.lbl') as Element, tr.t('legend.title'));
|
|
379
330
|
attr(this.#legendHead, 'aria-label', tr.t('legend.toggle'));
|
|
380
331
|
relabel(this.#zoomIn, `${tr.t('action.zoomIn')} (+)`);
|
|
@@ -437,26 +388,8 @@ export class Stage {
|
|
|
437
388
|
setText(this.#liveCanvasLabel, '');
|
|
438
389
|
}
|
|
439
390
|
|
|
440
|
-
#renderMode(
|
|
441
|
-
const
|
|
442
|
-
const mode = c.mode;
|
|
443
|
-
this.#stepper.hidden = mode !== 'changes';
|
|
444
|
-
this.#hopButtons.forEach((b, n) => {
|
|
445
|
-
attr(b, 'aria-checked', String(n === c.hops));
|
|
446
|
-
b.tabIndex = n === c.hops ? 0 : -1;
|
|
447
|
-
attr(b, 'aria-label', `${tr.t('focus.hops')}: ${n}`);
|
|
448
|
-
});
|
|
449
|
-
const group = c.focus?.collapsed[c.nearestGroup()];
|
|
450
|
-
this.#expandButton.hidden = !group;
|
|
451
|
-
if (group) {
|
|
452
|
-
const usable = c.canExpand(c.nearestGroup()) || group.nodes.length > 0;
|
|
453
|
-
this.#expandButton.disabled = !usable;
|
|
454
|
-
setText(this.#expandButton.querySelector('.n') as Element, tr.num(group.count));
|
|
455
|
-
relabel(
|
|
456
|
-
this.#expandButton,
|
|
457
|
-
`${usable ? tr.t('focus.expandNearest') : tr.t('focus.expandUnavailable')}: ${group.label} (E)`,
|
|
458
|
-
);
|
|
459
|
-
}
|
|
391
|
+
#renderMode(): void {
|
|
392
|
+
const mode = this.#ctx.controller.mode;
|
|
460
393
|
this.#transport.hidden = mode !== 'replay';
|
|
461
394
|
this.#caption.hidden = mode !== 'replay';
|
|
462
395
|
this.#swipe.hidden = mode !== 'swipe';
|
|
@@ -532,6 +465,8 @@ export class Stage {
|
|
|
532
465
|
}
|
|
533
466
|
const finished = clock.finished;
|
|
534
467
|
this.#finished.hidden = !finished;
|
|
468
|
+
// The finished pill takes the caption's place above the transport.
|
|
469
|
+
this.#caption.hidden = finished;
|
|
535
470
|
if (finished && this.#finished.childElementCount === 0) {
|
|
536
471
|
replace(
|
|
537
472
|
this.#finished,
|
package/src/ui/swatch.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Family swatches: the node cue of each family (docs/visual-language.md §Meaning families), drawn
|
|
3
|
+
* the same in the legend and on the sidebar's filter chips so the chips double as a legend.
|
|
4
|
+
*/
|
|
5
|
+
import type { Family } from '@kgdiff/scene';
|
|
6
|
+
|
|
7
|
+
export function swatch(family: Family): SVGSVGElement {
|
|
8
|
+
const v = (name: string) => `var(--kg-${name})`;
|
|
9
|
+
const c = family === 'neutral' ? v('neutral') : v(family);
|
|
10
|
+
let body: string;
|
|
11
|
+
switch (family) {
|
|
12
|
+
case 'added':
|
|
13
|
+
body = `<circle cx="11" cy="11" r="8.5" fill="none" stroke="${c}" stroke-opacity=".35" stroke-width="2"/><circle cx="11" cy="11" r="5" fill="${c}"/>`;
|
|
14
|
+
break;
|
|
15
|
+
case 'removed':
|
|
16
|
+
body = `<circle cx="11" cy="11" r="5.25" fill="none" stroke="${c}" stroke-width="1.5" stroke-dasharray="3 2.5"/>`;
|
|
17
|
+
break;
|
|
18
|
+
case 'identity':
|
|
19
|
+
body = `<circle cx="11" cy="11" r="9.5" fill="none" stroke="${c}" stroke-opacity=".3" stroke-width="1.5"/><circle cx="11" cy="11" r="7.25" fill="none" stroke="${c}" stroke-width="1.5"/><circle cx="11" cy="11" r="4.25" fill="${c}"/>`;
|
|
20
|
+
break;
|
|
21
|
+
case 'content':
|
|
22
|
+
case 'structure':
|
|
23
|
+
body = `<circle cx="11" cy="11" r="7.75" fill="none" stroke="${c}" stroke-width="1.5"/><circle cx="11" cy="11" r="4.5" fill="${c}"/>`;
|
|
24
|
+
break;
|
|
25
|
+
default:
|
|
26
|
+
body = `<circle cx="11" cy="11" r="3.5" fill="${c}"/>`;
|
|
27
|
+
}
|
|
28
|
+
const tpl = document.createElement('template');
|
|
29
|
+
tpl.innerHTML = `<svg class="swatch" viewBox="0 0 22 22" aria-hidden="true" focusable="false">${body}</svg>`;
|
|
30
|
+
return tpl.content.firstElementChild as SVGSVGElement;
|
|
31
|
+
}
|