@kgdiff/viewer 0.3.0 → 0.5.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/context.d.ts +42 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +87 -0
- package/dist/context.js.map +1 -0
- package/dist/controller.d.ts +30 -173
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +94 -1099
- package/dist/controller.js.map +1 -1
- package/dist/element.d.ts +12 -6
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +105 -236
- 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 +24 -10
- 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/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +3 -2
- package/dist/pipeline.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/root.d.ts +54 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +265 -0
- package/dist/root.js.map +1 -0
- package/dist/schedule.d.ts +22 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +41 -0
- package/dist/schedule.js.map +1 -0
- package/dist/session.d.ts +253 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +1239 -0
- package/dist/session.js.map +1 -0
- package/dist/standalone/kgdiff-viewer-compute.js +1 -1
- package/dist/standalone/kgdiff-viewer.js +4 -4
- package/dist/styles.d.ts +13 -6
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +270 -295
- package/dist/styles.js.map +1 -1
- package/dist/theme.d.ts +25 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +149 -0
- package/dist/theme.js.map +1 -0
- package/dist/ui/change-list.d.ts +18 -0
- package/dist/ui/change-list.d.ts.map +1 -0
- package/dist/ui/change-list.js +331 -0
- package/dist/ui/change-list.js.map +1 -0
- package/dist/ui/context.d.ts +21 -5
- 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/filters.d.ts +9 -0
- package/dist/ui/filters.d.ts.map +1 -0
- package/dist/ui/filters.js +68 -0
- package/dist/ui/filters.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 +26 -135
- 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/search.d.ts +13 -0
- package/dist/ui/search.d.ts.map +1 -0
- package/dist/ui/search.js +191 -0
- package/dist/ui/search.js.map +1 -0
- package/dist/ui/sidebar.d.ts +8 -16
- package/dist/ui/sidebar.d.ts.map +1 -1
- package/dist/ui/sidebar.js +15 -538
- 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 +42 -95
- 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/context.ts +106 -0
- package/src/controller.ts +120 -1196
- package/src/element.ts +114 -254
- package/src/hash-sync.ts +20 -6
- package/src/i18n.ts +15 -14
- package/src/index.ts +24 -10
- package/src/keyboard.ts +12 -6
- package/src/pipeline.ts +4 -1
- package/src/prefs.ts +19 -4
- package/src/root.ts +308 -0
- package/src/schedule.ts +43 -0
- package/src/session.ts +1386 -0
- package/src/styles.ts +277 -299
- package/src/theme.ts +167 -0
- package/src/ui/change-list.ts +401 -0
- package/src/ui/context.ts +23 -5
- package/src/ui/dock.ts +198 -0
- package/src/ui/filters.ts +79 -0
- package/src/ui/header.ts +40 -170
- package/src/ui/popovers.ts +3 -2
- package/src/ui/search.ts +220 -0
- package/src/ui/sidebar.ts +28 -667
- package/src/ui/stage.ts +50 -115
- package/src/ui/swatch.ts +31 -0
package/src/element.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<kgdiff-viewer
|
|
3
|
-
*
|
|
2
|
+
* `<kgdiff-viewer>`, the default layout (RFC 0001 §4): a root (the session, keyboard map, live
|
|
3
|
+
* region, events and theme tokens; `root.ts`) and the regions bound to it, the stage with its
|
|
4
|
+
* chrome, the change list, the search, the family filters and the detail panel, placed as they
|
|
5
|
+
* always were (a 360 px sidebar; a bottom sheet below 760 px). Around them: attribute /
|
|
6
|
+
* property reflection, lifecycle, the public methods and deep links.
|
|
4
7
|
*/
|
|
5
8
|
import { type Delta, KgdiffError, type Locale } from '@kgdiff/core';
|
|
6
9
|
import type { PaletteName } from '@kgdiff/scene';
|
|
7
|
-
import
|
|
10
|
+
import type { ViewerController } from './controller.js';
|
|
8
11
|
import { h } from './dom.js';
|
|
9
12
|
import { formatHash, parseHash } from './hash-sync.js';
|
|
10
13
|
import { detectLocale, translate, translator } from './i18n.js';
|
|
@@ -18,35 +21,41 @@ import type {
|
|
|
18
21
|
ViewerChrome,
|
|
19
22
|
ViewerLegend,
|
|
20
23
|
ViewerMode,
|
|
24
|
+
ViewerScope,
|
|
21
25
|
} from './index.js';
|
|
22
|
-
import { isTypingTarget, keyAction } from './keyboard.js';
|
|
23
26
|
import { assertPayload } from './payload.js';
|
|
24
27
|
import {
|
|
25
28
|
DEFAULT_LOOP_DELAY,
|
|
26
|
-
|
|
29
|
+
LEGACY_CHANGES_MODE,
|
|
27
30
|
toChrome,
|
|
28
31
|
toDelay,
|
|
29
32
|
toLegend,
|
|
30
33
|
toLocale,
|
|
31
34
|
toMode,
|
|
32
35
|
toPalette,
|
|
36
|
+
toScope,
|
|
33
37
|
toTheme,
|
|
34
38
|
} from './prefs.js';
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import
|
|
39
|
+
import { bindRegion, uiContext, ViewerRoot } from './root.js';
|
|
40
|
+
import { ALL_TOPICS, type Topic } from './session.js';
|
|
41
|
+
import { ChangeList } from './ui/change-list.js';
|
|
42
|
+
import type { UiActions } from './ui/context.js';
|
|
43
|
+
import { Detail } from './ui/detail.js';
|
|
44
|
+
import { Filters } from './ui/filters.js';
|
|
38
45
|
import { Header } from './ui/header.js';
|
|
39
46
|
import { HelpDialog, Menu } from './ui/popovers.js';
|
|
47
|
+
import { Search } from './ui/search.js';
|
|
40
48
|
import { Sidebar } from './ui/sidebar.js';
|
|
41
49
|
import { Stage } from './ui/stage.js';
|
|
42
50
|
|
|
43
|
-
/** Below this width the sidebar becomes a bottom sheet
|
|
51
|
+
/** Below this width the sidebar becomes a bottom sheet. */
|
|
44
52
|
export const COMPACT_WIDTH = 760;
|
|
45
|
-
/** Below this width the
|
|
53
|
+
/** Below this width the title drops the version tags. */
|
|
46
54
|
const NARROW_WIDTH = 1100;
|
|
47
55
|
|
|
48
56
|
const OBSERVED = [
|
|
49
57
|
'mode',
|
|
58
|
+
'scope',
|
|
50
59
|
'theme',
|
|
51
60
|
'palette',
|
|
52
61
|
'locale',
|
|
@@ -60,6 +69,7 @@ const OBSERVED = [
|
|
|
60
69
|
] as const;
|
|
61
70
|
const UPGRADED = [
|
|
62
71
|
'mode',
|
|
72
|
+
'scope',
|
|
63
73
|
'theme',
|
|
64
74
|
'palette',
|
|
65
75
|
'locale',
|
|
@@ -96,31 +106,6 @@ function deepFreeze<T>(value: T): T {
|
|
|
96
106
|
return value;
|
|
97
107
|
}
|
|
98
108
|
|
|
99
|
-
function media(query: string): MediaQueryList | null {
|
|
100
|
-
return typeof window !== 'undefined' && typeof window.matchMedia === 'function'
|
|
101
|
-
? window.matchMedia(query)
|
|
102
|
-
: null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Resolve once the browser had a chance to paint the loading step. Hidden tabs never run
|
|
107
|
-
* animation frames, so a short timer resolves it there (loading must never stall).
|
|
108
|
-
*/
|
|
109
|
-
function nextPaint(): Promise<void> {
|
|
110
|
-
return new Promise((resolve) => {
|
|
111
|
-
let done = false;
|
|
112
|
-
const finish = (): void => {
|
|
113
|
-
if (done) return;
|
|
114
|
-
done = true;
|
|
115
|
-
resolve();
|
|
116
|
-
};
|
|
117
|
-
if (typeof requestAnimationFrame === 'function') {
|
|
118
|
-
requestAnimationFrame(() => setTimeout(finish, 0));
|
|
119
|
-
}
|
|
120
|
-
setTimeout(finish, 50);
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
109
|
export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
125
110
|
static get observedAttributes(): readonly string[] {
|
|
126
111
|
return OBSERVED;
|
|
@@ -129,18 +114,17 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
129
114
|
/** @internal */
|
|
130
115
|
readonly controller: ViewerController;
|
|
131
116
|
readonly #shadow: ShadowRoot;
|
|
117
|
+
readonly #root: ViewerRoot;
|
|
132
118
|
#ui: {
|
|
133
119
|
root: HTMLElement;
|
|
134
|
-
vars: HTMLStyleElement;
|
|
135
120
|
header: Header;
|
|
136
121
|
stage: Stage;
|
|
122
|
+
list: ChangeList;
|
|
137
123
|
sidebar: Sidebar;
|
|
138
124
|
menu: Menu;
|
|
139
125
|
help: HelpDialog;
|
|
140
|
-
live: HTMLElement;
|
|
141
126
|
} | null = null;
|
|
142
127
|
#currentPayload: ReportPayload | null = null;
|
|
143
|
-
#renderFailed = false;
|
|
144
128
|
#reflecting = false;
|
|
145
129
|
#compact = false;
|
|
146
130
|
readonly #cleanup: (() => void)[] = [];
|
|
@@ -152,34 +136,13 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
152
136
|
constructor() {
|
|
153
137
|
super();
|
|
154
138
|
this.#shadow = this.attachShadow({ mode: 'open' });
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
select: (hunk, node) => {
|
|
158
|
-
this.#emit('kgdiff-select', { hunk, node });
|
|
159
|
-
this.#writeHash();
|
|
160
|
-
},
|
|
161
|
-
mode: (mode) => {
|
|
162
|
-
this.#emit('kgdiff-mode', { mode });
|
|
163
|
-
this.#writeHash();
|
|
164
|
-
},
|
|
165
|
-
review: (hunk, reviewed, progress) =>
|
|
166
|
-
this.#emit('kgdiff-review', { hunk, reviewed, progress }),
|
|
167
|
-
error: (error) => this.#emit('kgdiff-error', { error }),
|
|
168
|
-
announce: (text) => this.#announce(text),
|
|
169
|
-
replayEnd: (loop) => this.#emit('kgdiff-replay-end', { loop }),
|
|
170
|
-
};
|
|
171
|
-
this.controller = new ViewerController(host, {
|
|
172
|
-
now: () => (typeof performance !== 'undefined' ? performance.now() : Date.now()),
|
|
173
|
-
requestFrame: (cb) =>
|
|
174
|
-
typeof requestAnimationFrame === 'function' ? requestAnimationFrame(cb) : 0,
|
|
175
|
-
cancelFrame: (id) => {
|
|
176
|
-
if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(id);
|
|
177
|
-
},
|
|
178
|
-
yieldToPaint: nextPaint,
|
|
179
|
-
storage: defaultStorage(),
|
|
139
|
+
this.#root = new ViewerRoot(this, this.#shadow, this.#actions(), (type) => {
|
|
140
|
+
if (type === 'kgdiff-select' || type === 'kgdiff-mode') this.#writeHash();
|
|
180
141
|
});
|
|
142
|
+
this.controller = this.#root.controller;
|
|
181
143
|
this.controller.locale = toLocale(this.getAttribute('locale')) ?? detectLocale();
|
|
182
|
-
|
|
144
|
+
// The layout renders first, then the regions in the order they bound to the root.
|
|
145
|
+
this.#root.attach({ update: (topics) => this.#update(topics) });
|
|
183
146
|
}
|
|
184
147
|
|
|
185
148
|
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
@@ -196,6 +159,8 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
196
159
|
}
|
|
197
160
|
}
|
|
198
161
|
}
|
|
162
|
+
// Provide the session (the regions bind to it) and follow the system's preferences.
|
|
163
|
+
this.#cleanup.push(this.#root.connect());
|
|
199
164
|
if (!this.#ui) this.#build();
|
|
200
165
|
connected.push(this);
|
|
201
166
|
const ui = this.#ui;
|
|
@@ -211,35 +176,17 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
211
176
|
canvas.addEventListener('pointerdown', focusCanvas);
|
|
212
177
|
this.#cleanup.push(() => canvas.removeEventListener('pointerdown', focusCanvas));
|
|
213
178
|
}
|
|
214
|
-
this.#watchMedia();
|
|
215
179
|
this.#watchSize();
|
|
216
180
|
const onKey = (e: KeyboardEvent): void => this.#onKeyDown(e);
|
|
217
181
|
document.addEventListener('keydown', onKey);
|
|
218
182
|
this.#cleanup.push(() => document.removeEventListener('keydown', onKey));
|
|
219
|
-
const onStorage = (e: StorageEvent): void => this.controller.syncReview(e.key);
|
|
220
|
-
window.addEventListener('storage', onStorage);
|
|
221
|
-
this.#cleanup.push(() => window.removeEventListener('storage', onStorage));
|
|
222
183
|
const onHash = (): void => this.#readHash();
|
|
223
184
|
window.addEventListener('hashchange', onHash);
|
|
224
185
|
this.#cleanup.push(() => window.removeEventListener('hashchange', onHash));
|
|
225
186
|
this.controller.setChrome(() => ui.stage.chromeLayout());
|
|
226
|
-
document.fonts?.ready?.then(() => this.#ui?.
|
|
187
|
+
document.fonts?.ready?.then(() => this.#ui?.stage.layout()).catch(() => undefined);
|
|
227
188
|
this.#readHash();
|
|
228
|
-
this.#render(
|
|
229
|
-
new Set<Topic>([
|
|
230
|
-
'status',
|
|
231
|
-
'data',
|
|
232
|
-
'mode',
|
|
233
|
-
'theme',
|
|
234
|
-
'locale',
|
|
235
|
-
'filters',
|
|
236
|
-
'review',
|
|
237
|
-
'selection',
|
|
238
|
-
'focus',
|
|
239
|
-
'swipe',
|
|
240
|
-
'replay',
|
|
241
|
-
]),
|
|
242
|
-
);
|
|
189
|
+
this.#root.render(ALL_TOPICS);
|
|
243
190
|
}
|
|
244
191
|
|
|
245
192
|
disconnectedCallback(): void {
|
|
@@ -256,6 +203,7 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
256
203
|
if (this.#reflecting) return;
|
|
257
204
|
switch (name) {
|
|
258
205
|
case 'mode':
|
|
206
|
+
case 'scope':
|
|
259
207
|
case 'theme':
|
|
260
208
|
case 'palette':
|
|
261
209
|
case 'locale':
|
|
@@ -286,23 +234,39 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
286
234
|
|
|
287
235
|
/**
|
|
288
236
|
* Apply a preference from an attribute or a property. Valid values are explicit (a report's
|
|
289
|
-
* `view` does not override them); invalid ones fall back to the default (`overlay`, `
|
|
290
|
-
* `default`, the browser's language). Properties reflect the normalized value
|
|
237
|
+
* `view` does not override them); invalid ones fall back to the default (`overlay`, `all`,
|
|
238
|
+
* `auto`, `default`, the browser's language). Properties reflect the normalized value; the
|
|
239
|
+
* legacy mode `changes` reflects as `overlay` + scope `changes`.
|
|
291
240
|
*/
|
|
292
241
|
#applyPreference(
|
|
293
|
-
name: 'mode' | 'theme' | 'palette' | 'locale',
|
|
242
|
+
name: 'mode' | 'scope' | 'theme' | 'palette' | 'locale',
|
|
294
243
|
raw: unknown,
|
|
295
244
|
reflect: boolean,
|
|
296
245
|
): void {
|
|
297
246
|
const c = this.controller;
|
|
247
|
+
// `mode="changes"` is rewritten to the attribute pair it stands for.
|
|
248
|
+
const legacy = name === 'mode' && raw === LEGACY_CHANGES_MODE;
|
|
298
249
|
let value: string | null;
|
|
299
250
|
switch (name) {
|
|
300
251
|
case 'mode': {
|
|
252
|
+
if (legacy) {
|
|
253
|
+
c.explicit.add('scope');
|
|
254
|
+
c.setView('overlay', 'changes');
|
|
255
|
+
value = 'overlay';
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
301
258
|
const v = toMode(raw);
|
|
302
259
|
c.setMode(v ?? 'overlay');
|
|
303
260
|
value = v;
|
|
304
261
|
break;
|
|
305
262
|
}
|
|
263
|
+
case 'scope': {
|
|
264
|
+
const v = toScope(raw);
|
|
265
|
+
c.setScope(v ?? 'all');
|
|
266
|
+
// A locked scope stays `changes`: the attribute says what is shown.
|
|
267
|
+
value = v === null ? null : c.scope;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
306
270
|
case 'theme': {
|
|
307
271
|
const v = toTheme(raw);
|
|
308
272
|
c.setThemePreference(v ?? 'auto');
|
|
@@ -326,7 +290,8 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
326
290
|
}
|
|
327
291
|
if (value === null) c.explicit.delete(name);
|
|
328
292
|
else c.explicit.add(name);
|
|
329
|
-
if (reflect) this.#reflect(name, value);
|
|
293
|
+
if (reflect || legacy) this.#reflect(name, value);
|
|
294
|
+
if (legacy) this.#reflect('scope', c.scope);
|
|
330
295
|
}
|
|
331
296
|
|
|
332
297
|
// ─── Properties ───────────────────────────────────────────────────────────
|
|
@@ -335,10 +300,18 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
335
300
|
return this.controller.mode;
|
|
336
301
|
}
|
|
337
302
|
|
|
338
|
-
set mode(value: ViewerMode) {
|
|
303
|
+
set mode(value: ViewerMode | typeof LEGACY_CHANGES_MODE) {
|
|
339
304
|
this.#applyPreference('mode', value, true);
|
|
340
305
|
}
|
|
341
306
|
|
|
307
|
+
get scope(): ViewerScope {
|
|
308
|
+
return this.controller.scope;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
set scope(value: ViewerScope) {
|
|
312
|
+
this.#applyPreference('scope', value, true);
|
|
313
|
+
}
|
|
314
|
+
|
|
342
315
|
get theme(): ThemePreference {
|
|
343
316
|
return this.controller.themePreference;
|
|
344
317
|
}
|
|
@@ -481,10 +454,6 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
481
454
|
|
|
482
455
|
// ─── Internals ────────────────────────────────────────────────────────────
|
|
483
456
|
|
|
484
|
-
#emit(type: string, detail: unknown): void {
|
|
485
|
-
this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
|
|
486
|
-
}
|
|
487
|
-
|
|
488
457
|
#reflect(name: string, value: string | null): void {
|
|
489
458
|
if (this.getAttribute(name) === value) return;
|
|
490
459
|
this.#reflecting = true;
|
|
@@ -496,16 +465,6 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
496
465
|
}
|
|
497
466
|
}
|
|
498
467
|
|
|
499
|
-
#announce(text: string): void {
|
|
500
|
-
const live = this.#ui?.live;
|
|
501
|
-
if (!live) return;
|
|
502
|
-
live.textContent = '';
|
|
503
|
-
// A fresh text node in a later task makes screen readers announce repeats too.
|
|
504
|
-
setTimeout(() => {
|
|
505
|
-
live.textContent = text;
|
|
506
|
-
}, 30);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
468
|
get #sidebarOpen(): boolean {
|
|
510
469
|
return this.getAttribute('sidebar') !== 'closed';
|
|
511
470
|
}
|
|
@@ -545,16 +504,12 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
545
504
|
this.controller.hold(this.#pointerOver && this.loop && this.pauseOnHover);
|
|
546
505
|
}
|
|
547
506
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const ctx: UiContext = {
|
|
552
|
-
controller: this.controller,
|
|
507
|
+
/** What the layout does for the regions: preferences reflect as attributes, panels open. */
|
|
508
|
+
#actions(): UiActions {
|
|
509
|
+
return {
|
|
553
510
|
tr: () => translator(this.controller.locale),
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
this.controller.setMode(mode);
|
|
557
|
-
},
|
|
511
|
+
setMode: (mode) => this.controller.setMode(mode),
|
|
512
|
+
toggleScope: () => this.controller.toggleScope(),
|
|
558
513
|
setThemePreference: (pref) => {
|
|
559
514
|
this.theme = pref;
|
|
560
515
|
},
|
|
@@ -570,23 +525,27 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
570
525
|
openHelp: () => this.#ui?.help.show(),
|
|
571
526
|
focusSearch: () => this.#focusSearch(),
|
|
572
527
|
};
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
);
|
|
588
|
-
|
|
589
|
-
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
#build(): void {
|
|
531
|
+
const root = h('div', { class: 'root' });
|
|
532
|
+
this.#shadow.append(root);
|
|
533
|
+
// Each region binds to the root on its own (context protocol); the layout only places them.
|
|
534
|
+
const header = bindRegion(root, (ui) => new Header(ui));
|
|
535
|
+
const list = bindRegion(root, (ui) => new ChangeList(ui));
|
|
536
|
+
const search = bindRegion(root, (ui) => new Search(ui));
|
|
537
|
+
const filters = bindRegion(root, (ui) => new Filters(ui));
|
|
538
|
+
const detail = bindRegion(root, (ui) => new Detail(ui));
|
|
539
|
+
const stage = bindRegion(root, (ui) => new Stage(ui));
|
|
540
|
+
const ui = uiContext(this.#root, this.#shadow);
|
|
541
|
+
const menu = new Menu(ui);
|
|
542
|
+
const help = new HelpDialog(ui);
|
|
543
|
+
const sidebar = new Sidebar(ui.tr, list, search, filters, detail);
|
|
544
|
+
// The title and the actions float over the canvas; the menu drops from the actions.
|
|
545
|
+
header.attachMenu(menu.el);
|
|
546
|
+
stage.el.appendChild(header.el);
|
|
547
|
+
root.append(h('div', { class: 'body' }, stage.el, sidebar.el), help.el, this.#root.live);
|
|
548
|
+
this.#ui = { root, header, stage, list, sidebar, menu, help };
|
|
590
549
|
// Close the menu on outside clicks.
|
|
591
550
|
root.addEventListener('pointerdown', (e) => {
|
|
592
551
|
if (!menu.open) return;
|
|
@@ -604,27 +563,14 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
604
563
|
this.#applyChrome();
|
|
605
564
|
}
|
|
606
565
|
|
|
607
|
-
/**
|
|
608
|
-
#
|
|
609
|
-
try {
|
|
610
|
-
this.#renderTopics(topics);
|
|
611
|
-
this.#renderFailed = false;
|
|
612
|
-
} catch (error) {
|
|
613
|
-
console.error('[kgdiff] render failed:', error);
|
|
614
|
-
if (this.#renderFailed) return;
|
|
615
|
-
this.#renderFailed = true;
|
|
616
|
-
const c = this.controller;
|
|
617
|
-
c.explicit.clear();
|
|
618
|
-
c.setThemePreference('auto');
|
|
619
|
-
c.setPalette('default');
|
|
620
|
-
c.fail(error instanceof Error ? error : new Error(String(error)));
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
#renderTopics(topics: ReadonlySet<Topic>): void {
|
|
566
|
+
/** The layout's part of a render (the root renders the regions after it). */
|
|
567
|
+
#update(topics: ReadonlySet<Topic>): void {
|
|
625
568
|
const ui = this.#ui;
|
|
626
569
|
const c = this.controller;
|
|
627
|
-
if (topics.has('mode'))
|
|
570
|
+
if (topics.has('mode')) {
|
|
571
|
+
this.#reflect('mode', c.mode);
|
|
572
|
+
this.#reflect('scope', c.scope);
|
|
573
|
+
}
|
|
628
574
|
if (topics.has('theme')) {
|
|
629
575
|
this.#reflect(
|
|
630
576
|
'theme',
|
|
@@ -632,20 +578,18 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
632
578
|
);
|
|
633
579
|
}
|
|
634
580
|
if (!ui) return;
|
|
635
|
-
if (topics.has('theme') || topics.has('status'))
|
|
636
|
-
ui.vars.textContent = `:host { ${themeVariables(c.theme)} }`;
|
|
637
|
-
ui.root.dataset.theme = c.themeName;
|
|
638
|
-
}
|
|
581
|
+
if (topics.has('theme') || topics.has('status')) ui.root.dataset.theme = c.themeName;
|
|
639
582
|
if (topics.has('locale') || topics.has('status')) {
|
|
640
583
|
ui.root.setAttribute('lang', c.locale);
|
|
641
584
|
if (ui.menu.open) ui.menu.render();
|
|
642
585
|
if (ui.help.open) ui.help.render();
|
|
643
586
|
}
|
|
644
587
|
if (topics.has('theme') && ui.menu.open) ui.menu.render();
|
|
645
|
-
ui.header.update(topics);
|
|
646
588
|
ui.sidebar.update(topics);
|
|
647
|
-
|
|
648
|
-
|
|
589
|
+
if (topics.has('mode') || topics.has('status')) {
|
|
590
|
+
ui.root.dataset.mode = c.mode;
|
|
591
|
+
ui.root.dataset.scope = c.scope;
|
|
592
|
+
}
|
|
649
593
|
}
|
|
650
594
|
|
|
651
595
|
#toggleSidebar(open?: boolean): void {
|
|
@@ -665,22 +609,7 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
665
609
|
|
|
666
610
|
#focusSearch(): void {
|
|
667
611
|
if (!this.#sidebarOpen) this.#toggleSidebar(true);
|
|
668
|
-
this.#
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
#watchMedia(): void {
|
|
672
|
-
const dark = media('(prefers-color-scheme: light)');
|
|
673
|
-
const motion = media('(prefers-reduced-motion: reduce)');
|
|
674
|
-
const apply = (): void => {
|
|
675
|
-
this.controller.setSystemDark(!(dark?.matches ?? false));
|
|
676
|
-
this.controller.setReducedMotion(motion?.matches ?? false);
|
|
677
|
-
};
|
|
678
|
-
apply();
|
|
679
|
-
for (const q of [dark, motion]) {
|
|
680
|
-
if (!q) continue;
|
|
681
|
-
q.addEventListener?.('change', apply);
|
|
682
|
-
this.#cleanup.push(() => q.removeEventListener?.('change', apply));
|
|
683
|
-
}
|
|
612
|
+
this.#root.focus('search');
|
|
684
613
|
}
|
|
685
614
|
|
|
686
615
|
#watchSize(): void {
|
|
@@ -695,8 +624,8 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
695
624
|
this.#compact = compact;
|
|
696
625
|
ui.root.dataset.compact = String(compact);
|
|
697
626
|
}
|
|
698
|
-
ui.
|
|
699
|
-
ui.
|
|
627
|
+
ui.stage.layout();
|
|
628
|
+
ui.list.renderWindow();
|
|
700
629
|
if (compact) ui.sidebar.syncSheet();
|
|
701
630
|
this.controller.layoutChanged();
|
|
702
631
|
});
|
|
@@ -727,82 +656,7 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
727
656
|
}
|
|
728
657
|
return;
|
|
729
658
|
}
|
|
730
|
-
|
|
731
|
-
if (isTypingTarget(target)) return;
|
|
732
|
-
const c = this.controller;
|
|
733
|
-
const action = keyAction(e, c.mode);
|
|
734
|
-
if (!action) return;
|
|
735
|
-
// Enter / Space on a focused control activate that control.
|
|
736
|
-
const el = target as Element | null;
|
|
737
|
-
if (
|
|
738
|
-
(e.key === 'Enter' || e.key === ' ') &&
|
|
739
|
-
el?.closest?.('button, a, summary, [role="radio"], [role="menuitemradio"]')
|
|
740
|
-
) {
|
|
741
|
-
return;
|
|
742
|
-
}
|
|
743
|
-
if (c.status !== 'ready' && !['help', 'theme', 'sidebar', 'escape'].includes(action.type))
|
|
744
|
-
return;
|
|
745
|
-
e.preventDefault();
|
|
746
|
-
switch (action.type) {
|
|
747
|
-
case 'mode':
|
|
748
|
-
c.setMode(action.mode);
|
|
749
|
-
break;
|
|
750
|
-
case 'next':
|
|
751
|
-
c.next();
|
|
752
|
-
break;
|
|
753
|
-
case 'prev':
|
|
754
|
-
c.prev();
|
|
755
|
-
break;
|
|
756
|
-
case 'first':
|
|
757
|
-
c.first();
|
|
758
|
-
break;
|
|
759
|
-
case 'last':
|
|
760
|
-
c.last();
|
|
761
|
-
break;
|
|
762
|
-
case 'fly':
|
|
763
|
-
c.flyToSelection();
|
|
764
|
-
break;
|
|
765
|
-
case 'escape':
|
|
766
|
-
if (!ui.sidebar.escape()) c.clearSelection();
|
|
767
|
-
break;
|
|
768
|
-
case 'play-pause':
|
|
769
|
-
c.togglePlay();
|
|
770
|
-
break;
|
|
771
|
-
case 'step':
|
|
772
|
-
c.stepReplay(action.direction);
|
|
773
|
-
break;
|
|
774
|
-
case 'divider':
|
|
775
|
-
c.setSplit(c.split + action.delta);
|
|
776
|
-
break;
|
|
777
|
-
case 'fit':
|
|
778
|
-
void c.fit();
|
|
779
|
-
break;
|
|
780
|
-
case 'zoom':
|
|
781
|
-
c.zoom(action.direction);
|
|
782
|
-
break;
|
|
783
|
-
case 'search':
|
|
784
|
-
this.#focusSearch();
|
|
785
|
-
break;
|
|
786
|
-
case 'review':
|
|
787
|
-
c.toggleReviewed();
|
|
788
|
-
break;
|
|
789
|
-
case 'sidebar':
|
|
790
|
-
this.#toggleSidebar();
|
|
791
|
-
break;
|
|
792
|
-
case 'theme': {
|
|
793
|
-
const i = THEME_PREFERENCES.indexOf(c.themePreference);
|
|
794
|
-
this.theme = THEME_PREFERENCES[(i + 1) % THEME_PREFERENCES.length] as ThemePreference;
|
|
795
|
-
break;
|
|
796
|
-
}
|
|
797
|
-
case 'help':
|
|
798
|
-
ui.help.show();
|
|
799
|
-
break;
|
|
800
|
-
case 'expand':
|
|
801
|
-
c.expandNearest();
|
|
802
|
-
break;
|
|
803
|
-
default:
|
|
804
|
-
action satisfies never;
|
|
805
|
-
}
|
|
659
|
+
this.#root.handleKey(e);
|
|
806
660
|
}
|
|
807
661
|
|
|
808
662
|
// ─── Deep links ───────────────────────────────────────────────────────────
|
|
@@ -813,10 +667,12 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
813
667
|
const state = parseHash(location.hash);
|
|
814
668
|
this.#lastHash = location.hash;
|
|
815
669
|
if (!state) return;
|
|
816
|
-
if (state.mode) {
|
|
817
|
-
// A deep link outranks the large-graph default and a report's preferred
|
|
818
|
-
this.controller
|
|
819
|
-
|
|
670
|
+
if (state.mode || state.scope) {
|
|
671
|
+
// A deep link outranks the large-graph default and a report's preferred view.
|
|
672
|
+
const c = this.controller;
|
|
673
|
+
if (state.mode) c.explicit.add('mode');
|
|
674
|
+
if (state.scope) c.explicit.add('scope');
|
|
675
|
+
c.setView(state.mode ?? c.mode, state.scope ?? c.scope);
|
|
820
676
|
}
|
|
821
677
|
if (state.hunk) this.controller.selectHunkById(state.hunk, { fly: true });
|
|
822
678
|
}
|
|
@@ -825,7 +681,11 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
825
681
|
if (!this.hasAttribute('sync-hash') || typeof location === 'undefined') return;
|
|
826
682
|
const c = this.controller;
|
|
827
683
|
const hunk = c.selectedHunk >= 0 ? c.scene?.hunks[c.selectedHunk]?.id : undefined;
|
|
828
|
-
const hash = formatHash({
|
|
684
|
+
const hash = formatHash({
|
|
685
|
+
...(hunk !== undefined ? { hunk } : {}),
|
|
686
|
+
mode: c.mode,
|
|
687
|
+
scope: c.scope,
|
|
688
|
+
});
|
|
829
689
|
if (hash === location.hash) return;
|
|
830
690
|
this.#lastHash = hash;
|
|
831
691
|
try {
|
package/src/hash-sync.ts
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Deep links for `sync-hash`: `#kgdiff=hunk:<id>;mode:<mode>`.
|
|
2
|
+
* Deep links for `sync-hash`: `#kgdiff=hunk:<id>;mode:<mode>;scope:<scope>`. The legacy
|
|
3
|
+
* `mode:changes` reads as overlay + the `changes` scope.
|
|
3
4
|
*
|
|
4
5
|
* Values are percent-encoded (hunk ids embed union ids, which may be IRIs containing `;`, `#`
|
|
5
6
|
* or `:`), so parsing splits on the first `:` of each `;`-separated field. Unknown fields are
|
|
6
7
|
* ignored; a malformed value drops just that field.
|
|
7
8
|
*/
|
|
8
|
-
import type { ViewerMode } from './index.js';
|
|
9
|
+
import type { ViewerMode, ViewerScope } from './index.js';
|
|
10
|
+
import { LEGACY_CHANGES_MODE, toMode, toScope } from './prefs.js';
|
|
9
11
|
|
|
10
12
|
export interface HashState {
|
|
11
13
|
readonly hunk?: string;
|
|
12
14
|
readonly mode?: ViewerMode;
|
|
15
|
+
readonly scope?: ViewerScope;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
const PREFIX = 'kgdiff=';
|
|
16
|
-
const MODES: ReadonlySet<string> = new Set<ViewerMode>(['overlay', 'swipe', 'replay', 'changes']);
|
|
17
19
|
|
|
18
20
|
export function isViewerMode(value: unknown): value is ViewerMode {
|
|
19
|
-
return
|
|
21
|
+
return toMode(value) !== null;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
function decode(value: string): string | undefined {
|
|
@@ -33,6 +35,8 @@ export function parseHash(hash: string): HashState | null {
|
|
|
33
35
|
if (!raw.startsWith(PREFIX)) return null;
|
|
34
36
|
let hunk: string | undefined;
|
|
35
37
|
let mode: ViewerMode | undefined;
|
|
38
|
+
let scope: ViewerScope | undefined;
|
|
39
|
+
let legacy = false;
|
|
36
40
|
for (const field of raw.slice(PREFIX.length).split(';')) {
|
|
37
41
|
const colon = field.indexOf(':');
|
|
38
42
|
if (colon <= 0) continue;
|
|
@@ -40,9 +44,18 @@ export function parseHash(hash: string): HashState | null {
|
|
|
40
44
|
const value = decode(field.slice(colon + 1));
|
|
41
45
|
if (value === undefined || value === '') continue;
|
|
42
46
|
if (key === 'hunk') hunk = value;
|
|
43
|
-
else if (key === 'mode' &&
|
|
47
|
+
else if (key === 'mode' && value === LEGACY_CHANGES_MODE) {
|
|
48
|
+
mode = 'overlay';
|
|
49
|
+
legacy = true;
|
|
50
|
+
} else if (key === 'mode') mode = toMode(value) ?? mode;
|
|
51
|
+
else if (key === 'scope') scope = toScope(value) ?? scope;
|
|
44
52
|
}
|
|
45
|
-
|
|
53
|
+
if (legacy) scope ??= 'changes';
|
|
54
|
+
return {
|
|
55
|
+
...(hunk !== undefined ? { hunk } : {}),
|
|
56
|
+
...(mode !== undefined ? { mode } : {}),
|
|
57
|
+
...(scope !== undefined ? { scope } : {}),
|
|
58
|
+
};
|
|
46
59
|
}
|
|
47
60
|
|
|
48
61
|
/** Encode `state` as a hash (`#kgdiff=…`); empty state gives `#kgdiff=`. */
|
|
@@ -50,5 +63,6 @@ export function formatHash(state: HashState): string {
|
|
|
50
63
|
const fields: string[] = [];
|
|
51
64
|
if (state.hunk !== undefined) fields.push(`hunk:${encodeURIComponent(state.hunk)}`);
|
|
52
65
|
if (state.mode !== undefined) fields.push(`mode:${state.mode}`);
|
|
66
|
+
if (state.scope !== undefined) fields.push(`scope:${state.scope}`);
|
|
53
67
|
return `#${PREFIX}${fields.join(';')}`;
|
|
54
68
|
}
|