@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.
- 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/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 +26 -237
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +33 -1484
- 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 +51 -42
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +381 -602
- 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 +331 -36
- 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/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +3 -2
- package/dist/pipeline.js.map +1 -1
- 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 +54 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +288 -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 +269 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +1237 -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 +17 -6
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +367 -258
- package/dist/styles.js.map +1 -1
- package/dist/theme.d.ts +32 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +152 -0
- package/dist/theme.js.map +1 -0
- 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 +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 +34 -7
- package/dist/ui/context.d.ts.map +1 -1
- package/dist/ui/context.js.map +1 -1
- 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 +41 -3
- package/dist/ui/header.d.ts.map +1 -1
- package/dist/ui/header.js +101 -36
- 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 +13 -0
- package/dist/ui/search.d.ts.map +1 -0
- package/dist/ui/search.js +192 -0
- package/dist/ui/search.js.map +1 -0
- package/dist/ui/sidebar.d.ts +32 -17
- package/dist/ui/sidebar.d.ts.map +1 -1
- package/dist/ui/sidebar.js +327 -519
- 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/swatch.js +1 -1
- package/dist/ui/swatch.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/context.ts +106 -0
- package/src/controller.ts +52 -1630
- package/src/dom.ts +30 -0
- package/src/element.ts +386 -619
- 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 +350 -35
- 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/pipeline.ts +4 -1
- package/src/prefs.ts +12 -4
- package/src/react.ts +459 -0
- package/src/root-element.ts +624 -0
- package/src/root.ts +335 -0
- package/src/schedule.ts +43 -0
- package/src/session.ts +1375 -0
- package/src/styles.ts +396 -263
- package/src/theme.ts +170 -0
- package/src/topics.ts +37 -0
- package/src/ui/change-list.ts +405 -0
- package/src/ui/context.ts +36 -7
- package/src/ui/filters.ts +79 -0
- package/src/ui/header.ts +129 -58
- package/src/ui/legend.ts +112 -0
- package/src/ui/popovers.ts +68 -25
- package/src/ui/search.ts +225 -0
- package/src/ui/sidebar.ts +362 -639
- package/src/ui/stage.ts +267 -331
- package/src/ui/swatch.ts +1 -1
- 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
package/src/element.ts
CHANGED
|
@@ -1,46 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<kgdiff-viewer
|
|
3
|
-
*
|
|
2
|
+
* `<kgdiff-viewer>`, the default layout (RFC 0001 §4): a root (`root-element.ts`: the session,
|
|
3
|
+
* keyboard map, live region, events, theme tokens, preferences and methods) and the regions
|
|
4
|
+
* bound to it, the stage with its chrome, the change list, the search, the family filters and
|
|
5
|
+
* the detail panel, placed as they always were (a 360 px sidebar; a bottom sheet below 760 px).
|
|
6
|
+
*
|
|
7
|
+
* Every region has a panel state (`panels.ts`): the host's `panels` attribute or property and
|
|
8
|
+
* the presets `sidebar`, `header` and `chrome` set them, the user changes them (folding the
|
|
9
|
+
* sidebar, the list or the detail panel, opening the legend, dragging the sidebar's width or
|
|
10
|
+
* the list / detail split), and the layout remembers the user's choices, theme, palette and
|
|
11
|
+
* language per origin (`layout-store.ts`; `persist="none"` turns that off). `kgdiff-layout`
|
|
12
|
+
* reports every change.
|
|
4
13
|
*/
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import { h } from './dom.js';
|
|
9
|
-
import {
|
|
10
|
-
import { detectLocale, translate, translator } from './i18n.js';
|
|
14
|
+
|
|
15
|
+
import { provideContext } from './context.js';
|
|
16
|
+
import { ViewerController } from './controller.js';
|
|
17
|
+
import { h, isOpen } from './dom.js';
|
|
18
|
+
import { type StageViewContext, stageContext } from './elements/stage-context.js';
|
|
11
19
|
import type {
|
|
12
|
-
FitOptions,
|
|
13
20
|
KgdiffViewerElement,
|
|
14
21
|
LoadOptions,
|
|
22
|
+
PanelRegion,
|
|
23
|
+
PanelStates,
|
|
15
24
|
ReportPayload,
|
|
16
|
-
ReviewState,
|
|
17
|
-
ThemePreference,
|
|
18
25
|
ViewerChrome,
|
|
26
|
+
ViewerLayout,
|
|
19
27
|
ViewerLegend,
|
|
20
|
-
|
|
21
|
-
ViewerScope,
|
|
28
|
+
ViewerPanels,
|
|
22
29
|
} from './index.js';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
LEGACY_CHANGES_MODE,
|
|
28
|
-
THEME_PREFERENCES,
|
|
29
|
-
toChrome,
|
|
30
|
-
toDelay,
|
|
31
|
-
toLegend,
|
|
32
|
-
toLocale,
|
|
33
|
-
toMode,
|
|
34
|
-
toPalette,
|
|
35
|
-
toScope,
|
|
36
|
-
toTheme,
|
|
37
|
-
} from './prefs.js';
|
|
30
|
+
import { LAYOUT_KEY, type Remembered, readRemembered, writeRemembered } from './layout-store.js';
|
|
31
|
+
import { presetPanels, resolvePanels, SIDEBAR_REGIONS, samePanels } from './panel-levels.js';
|
|
32
|
+
import { DEFAULT_PANELS, formatPanels, PANEL_REGIONS, parsePanels, toPanels } from './panels.js';
|
|
33
|
+
import { sanitizeView, toChrome, toLegend } from './prefs.js';
|
|
38
34
|
import { defaultStorage } from './review-store.js';
|
|
39
|
-
import {
|
|
40
|
-
import
|
|
35
|
+
import { bindRegion, uiContext } from './root.js';
|
|
36
|
+
import { ROOT_ATTRIBUTES, RootElement } from './root-element.js';
|
|
37
|
+
import type { SessionDeps, SessionHost, Topic } from './session.js';
|
|
38
|
+
import { stylesheet } from './styles.js';
|
|
39
|
+
import { ChangeList } from './ui/change-list.js';
|
|
40
|
+
import type { UiActions } from './ui/context.js';
|
|
41
|
+
import { Detail } from './ui/detail.js';
|
|
42
|
+
import { Filters } from './ui/filters.js';
|
|
41
43
|
import { Header } from './ui/header.js';
|
|
42
44
|
import { HelpDialog, Menu } from './ui/popovers.js';
|
|
43
|
-
import {
|
|
45
|
+
import { Search } from './ui/search.js';
|
|
46
|
+
import { SIDEBAR_WIDTH, Sidebar } from './ui/sidebar.js';
|
|
44
47
|
import { Stage } from './ui/stage.js';
|
|
45
48
|
|
|
46
49
|
/** Below this width the sidebar becomes a bottom sheet. */
|
|
@@ -49,165 +52,96 @@ export const COMPACT_WIDTH = 760;
|
|
|
49
52
|
const NARROW_WIDTH = 1100;
|
|
50
53
|
|
|
51
54
|
const OBSERVED = [
|
|
52
|
-
|
|
53
|
-
'scope',
|
|
54
|
-
'theme',
|
|
55
|
-
'palette',
|
|
56
|
-
'locale',
|
|
55
|
+
...ROOT_ATTRIBUTES,
|
|
57
56
|
'sidebar',
|
|
58
|
-
'
|
|
57
|
+
'header',
|
|
59
58
|
'chrome',
|
|
60
59
|
'legend',
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
] as const;
|
|
65
|
-
const UPGRADED = [
|
|
66
|
-
'mode',
|
|
67
|
-
'scope',
|
|
68
|
-
'theme',
|
|
69
|
-
'palette',
|
|
70
|
-
'locale',
|
|
71
|
-
'chrome',
|
|
72
|
-
'legend',
|
|
73
|
-
'loop',
|
|
74
|
-
'loopDelay',
|
|
75
|
-
'pauseOnHover',
|
|
76
|
-
'payload',
|
|
60
|
+
'panels',
|
|
61
|
+
'persist',
|
|
62
|
+
'persist-key',
|
|
77
63
|
] as const;
|
|
78
64
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
'ArrowUp',
|
|
82
|
-
'ArrowDown',
|
|
83
|
-
'ArrowLeft',
|
|
84
|
-
'ArrowRight',
|
|
85
|
-
'Home',
|
|
86
|
-
'End',
|
|
87
|
-
'PageUp',
|
|
88
|
-
'PageDown',
|
|
89
|
-
' ',
|
|
90
|
-
'Spacebar',
|
|
91
|
-
]);
|
|
92
|
-
|
|
93
|
-
/** Connected viewers, in connection order (`global-shortcuts` goes to the first opted-in one). */
|
|
94
|
-
const connected: KgdiffViewer[] = [];
|
|
95
|
-
|
|
96
|
-
function deepFreeze<T>(value: T): T {
|
|
97
|
-
if (typeof value === 'object' && value !== null && !Object.isFrozen(value)) {
|
|
98
|
-
for (const v of Object.values(value)) deepFreeze(v);
|
|
99
|
-
Object.freeze(value);
|
|
100
|
-
}
|
|
101
|
-
return value;
|
|
102
|
-
}
|
|
65
|
+
type Preset = 'sidebar' | 'header' | 'chrome';
|
|
66
|
+
type MutablePanels = { -readonly [R in PanelRegion]?: PanelStates[R] };
|
|
103
67
|
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
? window.matchMedia(query)
|
|
107
|
-
: null;
|
|
68
|
+
function warnPanels(invalid: readonly string[]): void {
|
|
69
|
+
if (invalid.length > 0) console.warn(`[kgdiff] ignoring invalid panels: ${invalid.join(' ')}`);
|
|
108
70
|
}
|
|
109
71
|
|
|
110
72
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
73
|
+
* @tagname kgdiff-viewer
|
|
74
|
+
* @attr {'open' | 'closed'} sidebar - A preset: `closed` starts with the sidebar folded (`\` and the
|
|
75
|
+
* sidebar button open it), `open` with its four regions open. Reflects the sidebar's state
|
|
76
|
+
* once the user toggles it.
|
|
77
|
+
* @attr {'compact'} header - A preset: `compact` leaves the brand and the compared names out of
|
|
78
|
+
* the canvas, for hosts that show their own (`panels="title:hidden"`).
|
|
79
|
+
* @slot top-start - The page's content over the canvas, top left (under the title while it shows).
|
|
80
|
+
* @slot top-end - The page's content over the canvas, top right (under the actions while they show).
|
|
81
|
+
* @slot bottom-start - The page's content over the canvas, bottom left, above the legend.
|
|
82
|
+
* @slot bottom-center - The page's content above the view dock and the transport.
|
|
83
|
+
* @slot bottom-end - The page's content over the canvas, bottom right, above the zoom buttons and the minimap.
|
|
113
84
|
*/
|
|
114
|
-
|
|
115
|
-
return new Promise((resolve) => {
|
|
116
|
-
let done = false;
|
|
117
|
-
const finish = (): void => {
|
|
118
|
-
if (done) return;
|
|
119
|
-
done = true;
|
|
120
|
-
resolve();
|
|
121
|
-
};
|
|
122
|
-
if (typeof requestAnimationFrame === 'function') {
|
|
123
|
-
requestAnimationFrame(() => setTimeout(finish, 0));
|
|
124
|
-
}
|
|
125
|
-
setTimeout(finish, 50);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
85
|
+
export class KgdiffViewer extends RootElement<ViewerController> implements KgdiffViewerElement {
|
|
130
86
|
static get observedAttributes(): readonly string[] {
|
|
131
87
|
return OBSERVED;
|
|
132
88
|
}
|
|
133
89
|
|
|
134
|
-
/** @internal */
|
|
135
|
-
readonly controller: ViewerController;
|
|
136
|
-
readonly #shadow: ShadowRoot;
|
|
137
90
|
#ui: {
|
|
138
91
|
root: HTMLElement;
|
|
139
|
-
vars: HTMLStyleElement;
|
|
140
92
|
header: Header;
|
|
141
93
|
stage: Stage;
|
|
94
|
+
list: ChangeList;
|
|
142
95
|
sidebar: Sidebar;
|
|
143
96
|
menu: Menu;
|
|
144
97
|
help: HelpDialog;
|
|
145
|
-
live: HTMLElement;
|
|
146
98
|
} | null = null;
|
|
147
|
-
#currentPayload: ReportPayload | null = null;
|
|
148
|
-
#renderFailed = false;
|
|
149
|
-
#reflecting = false;
|
|
150
99
|
#compact = false;
|
|
151
100
|
readonly #cleanup: (() => void)[] = [];
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
#
|
|
101
|
+
|
|
102
|
+
// Panel states, from the first level that names a region (see `panels.ts`).
|
|
103
|
+
/** What the user changed in this page. */
|
|
104
|
+
readonly #runtime: MutablePanels = {};
|
|
105
|
+
/** The host's `panels`. */
|
|
106
|
+
#hostPanels: ViewerPanels = {};
|
|
107
|
+
/** The presets, from the attributes as the host set them (not as the layout reflects them). */
|
|
108
|
+
readonly #presetAttrs: Record<Preset, string | null> = {
|
|
109
|
+
sidebar: null,
|
|
110
|
+
header: null,
|
|
111
|
+
chrome: null,
|
|
112
|
+
};
|
|
113
|
+
#presets: ViewerPanels = {};
|
|
114
|
+
/** What the user chose before (`persist`), read at the first connection. */
|
|
115
|
+
#remembered: Remembered = { panels: {} };
|
|
116
|
+
/** The report's `view.panels`. */
|
|
117
|
+
#report: ViewerPanels = {};
|
|
118
|
+
#states: PanelStates = DEFAULT_PANELS;
|
|
119
|
+
/** The sidebar's regions that were open when the user folded it. */
|
|
120
|
+
#folded: readonly PanelRegion[] | null = null;
|
|
121
|
+
/** `kgdiff-layout` reports changes from the first connection on. */
|
|
122
|
+
#layoutReady = false;
|
|
123
|
+
readonly #storage = defaultStorage();
|
|
156
124
|
|
|
157
125
|
constructor() {
|
|
158
126
|
super();
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
ready: (stats) => this.#emit('kgdiff-ready', { stats }),
|
|
162
|
-
select: (hunk, node) => {
|
|
163
|
-
this.#emit('kgdiff-select', { hunk, node });
|
|
164
|
-
this.#writeHash();
|
|
165
|
-
},
|
|
166
|
-
mode: (mode, scope) => {
|
|
167
|
-
this.#emit('kgdiff-mode', { mode, scope });
|
|
168
|
-
this.#writeHash();
|
|
169
|
-
},
|
|
170
|
-
review: (hunk, reviewed, progress) =>
|
|
171
|
-
this.#emit('kgdiff-review', { hunk, reviewed, progress }),
|
|
172
|
-
error: (error) => this.#emit('kgdiff-error', { error }),
|
|
173
|
-
announce: (text) => this.#announce(text),
|
|
174
|
-
replayEnd: (loop) => this.#emit('kgdiff-replay-end', { loop }),
|
|
175
|
-
};
|
|
176
|
-
this.controller = new ViewerController(host, {
|
|
177
|
-
now: () => (typeof performance !== 'undefined' ? performance.now() : Date.now()),
|
|
178
|
-
requestFrame: (cb) =>
|
|
179
|
-
typeof requestAnimationFrame === 'function' ? requestAnimationFrame(cb) : 0,
|
|
180
|
-
cancelFrame: (id) => {
|
|
181
|
-
if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(id);
|
|
182
|
-
},
|
|
183
|
-
yieldToPaint: nextPaint,
|
|
184
|
-
storage: defaultStorage(),
|
|
185
|
-
});
|
|
186
|
-
this.controller.locale = toLocale(this.getAttribute('locale')) ?? detectLocale();
|
|
187
|
-
this.controller.subscribe((topics) => this.#render(topics));
|
|
127
|
+
// The layout renders first, then the regions in the order they bound to the root.
|
|
128
|
+
this.root.attach({ update: (topics) => this.#update(topics) });
|
|
188
129
|
}
|
|
189
130
|
|
|
190
131
|
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
191
132
|
|
|
192
|
-
|
|
193
|
-
if (!this.#upgraded) {
|
|
194
|
-
this.#upgraded = true;
|
|
195
|
-
// Properties set before the element was defined shadow the accessors: re-apply them.
|
|
196
|
-
for (const prop of UPGRADED) {
|
|
197
|
-
if (Object.hasOwn(this, prop)) {
|
|
198
|
-
const value = (this as unknown as Record<string, unknown>)[prop];
|
|
199
|
-
delete (this as unknown as Record<string, unknown>)[prop];
|
|
200
|
-
(this as unknown as Record<string, unknown>)[prop] = value;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
133
|
+
protected connected(): void {
|
|
204
134
|
if (!this.#ui) this.#build();
|
|
205
|
-
connected.push(this);
|
|
206
135
|
const ui = this.#ui;
|
|
207
136
|
if (!ui) return;
|
|
208
|
-
this
|
|
137
|
+
if (!this.#layoutReady) {
|
|
138
|
+
this.#loadRemembered();
|
|
139
|
+
this.#layoutReady = true;
|
|
140
|
+
}
|
|
141
|
+
// The stage adds its own minimap to the view (unless a slotted one replaces it).
|
|
142
|
+
this.controller.attach(ui.stage.canvasHost, null);
|
|
209
143
|
// The graph takes keyboard focus (shortcuts apply where focus is), also on click.
|
|
210
|
-
const canvas =
|
|
144
|
+
const canvas = this.#canvas();
|
|
211
145
|
if (canvas) {
|
|
212
146
|
canvas.tabIndex = this.chrome === 'none' ? -1 : 0;
|
|
213
147
|
const focusCanvas = (): void => {
|
|
@@ -216,191 +150,105 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
216
150
|
canvas.addEventListener('pointerdown', focusCanvas);
|
|
217
151
|
this.#cleanup.push(() => canvas.removeEventListener('pointerdown', focusCanvas));
|
|
218
152
|
}
|
|
219
|
-
this.#watchMedia();
|
|
220
153
|
this.#watchSize();
|
|
221
|
-
const onKey = (e: KeyboardEvent): void => this.#onKeyDown(e);
|
|
222
|
-
document.addEventListener('keydown', onKey);
|
|
223
|
-
this.#cleanup.push(() => document.removeEventListener('keydown', onKey));
|
|
224
|
-
const onStorage = (e: StorageEvent): void => this.controller.syncReview(e.key);
|
|
225
|
-
window.addEventListener('storage', onStorage);
|
|
226
|
-
this.#cleanup.push(() => window.removeEventListener('storage', onStorage));
|
|
227
|
-
const onHash = (): void => this.#readHash();
|
|
228
|
-
window.addEventListener('hashchange', onHash);
|
|
229
|
-
this.#cleanup.push(() => window.removeEventListener('hashchange', onHash));
|
|
230
154
|
this.controller.setChrome(() => ui.stage.chromeLayout());
|
|
231
155
|
document.fonts?.ready?.then(() => this.#ui?.stage.layout()).catch(() => undefined);
|
|
232
|
-
this.#readHash();
|
|
233
|
-
this.#render(
|
|
234
|
-
new Set<Topic>([
|
|
235
|
-
'status',
|
|
236
|
-
'data',
|
|
237
|
-
'mode',
|
|
238
|
-
'theme',
|
|
239
|
-
'locale',
|
|
240
|
-
'filters',
|
|
241
|
-
'review',
|
|
242
|
-
'selection',
|
|
243
|
-
'focus',
|
|
244
|
-
'swipe',
|
|
245
|
-
'replay',
|
|
246
|
-
'overview',
|
|
247
|
-
]),
|
|
248
|
-
);
|
|
249
156
|
}
|
|
250
157
|
|
|
251
|
-
|
|
252
|
-
const i = connected.indexOf(this);
|
|
253
|
-
if (i >= 0) connected.splice(i, 1);
|
|
158
|
+
protected disconnected(): void {
|
|
254
159
|
for (const fn of this.#cleanup.splice(0)) fn();
|
|
255
160
|
this.controller.detach();
|
|
256
|
-
// Moved while hovered: no pointerleave follows.
|
|
257
|
-
this.#pointerOver = false;
|
|
258
|
-
this.#applyHold();
|
|
259
161
|
}
|
|
260
162
|
|
|
261
|
-
|
|
262
|
-
|
|
163
|
+
protected override attributeChanged(
|
|
164
|
+
name: string,
|
|
165
|
+
value: string | null,
|
|
166
|
+
old: string | null,
|
|
167
|
+
): void {
|
|
263
168
|
switch (name) {
|
|
264
|
-
case 'mode':
|
|
265
|
-
case 'scope':
|
|
266
|
-
case 'theme':
|
|
267
|
-
case 'palette':
|
|
268
|
-
case 'locale':
|
|
269
|
-
this.#applyPreference(name, value, false);
|
|
270
|
-
break;
|
|
271
169
|
case 'sidebar':
|
|
272
|
-
|
|
170
|
+
// The layout reflects the sidebar's state here (after `\`): the page changing the
|
|
171
|
+
// attribute from what it says now is a command, even back to what the page said
|
|
172
|
+
// before; setting what it already says is not.
|
|
173
|
+
this.#preset(name, value, value !== old);
|
|
273
174
|
break;
|
|
274
|
-
case '
|
|
275
|
-
|
|
175
|
+
case 'header':
|
|
176
|
+
this.#preset(name, value);
|
|
276
177
|
break;
|
|
277
178
|
case 'chrome':
|
|
278
|
-
|
|
179
|
+
this.#preset(name, value);
|
|
279
180
|
this.#applyChrome();
|
|
280
181
|
break;
|
|
281
|
-
case '
|
|
282
|
-
|
|
283
|
-
this.controller.setLoop(this.loop ? this.loopDelay : null);
|
|
284
|
-
this.#applyHold();
|
|
285
|
-
break;
|
|
286
|
-
case 'pause-on-hover':
|
|
287
|
-
this.#applyHold();
|
|
288
|
-
break;
|
|
289
|
-
default:
|
|
290
|
-
break;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Apply a preference from an attribute or a property. Valid values are explicit (a report's
|
|
296
|
-
* `view` does not override them); invalid ones fall back to the default (`overlay`, `all`,
|
|
297
|
-
* `auto`, `default`, the browser's language). Properties reflect the normalized value; the
|
|
298
|
-
* legacy mode `changes` reflects as `overlay` + scope `changes`.
|
|
299
|
-
*/
|
|
300
|
-
#applyPreference(
|
|
301
|
-
name: 'mode' | 'scope' | 'theme' | 'palette' | 'locale',
|
|
302
|
-
raw: unknown,
|
|
303
|
-
reflect: boolean,
|
|
304
|
-
): void {
|
|
305
|
-
const c = this.controller;
|
|
306
|
-
// `mode="changes"` is rewritten to the attribute pair it stands for.
|
|
307
|
-
const legacy = name === 'mode' && raw === LEGACY_CHANGES_MODE;
|
|
308
|
-
let value: string | null;
|
|
309
|
-
switch (name) {
|
|
310
|
-
case 'mode': {
|
|
311
|
-
if (legacy) {
|
|
312
|
-
c.explicit.add('scope');
|
|
313
|
-
c.setView('overlay', 'changes');
|
|
314
|
-
value = 'overlay';
|
|
315
|
-
break;
|
|
316
|
-
}
|
|
317
|
-
const v = toMode(raw);
|
|
318
|
-
c.setMode(v ?? 'overlay');
|
|
319
|
-
value = v;
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
case 'scope': {
|
|
323
|
-
const v = toScope(raw);
|
|
324
|
-
c.setScope(v ?? 'all');
|
|
325
|
-
// A locked scope stays `changes`: the attribute says what is shown.
|
|
326
|
-
value = v === null ? null : c.scope;
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
case 'theme': {
|
|
330
|
-
const v = toTheme(raw);
|
|
331
|
-
c.setThemePreference(v ?? 'auto');
|
|
332
|
-
value = v;
|
|
182
|
+
case 'legend':
|
|
183
|
+
this.#applyChrome();
|
|
333
184
|
break;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
value = v;
|
|
185
|
+
case 'panels': {
|
|
186
|
+
const { panels, invalid } = parsePanels(value ?? '');
|
|
187
|
+
warnPanels(invalid);
|
|
188
|
+
this.#setHostPanels(panels);
|
|
339
189
|
break;
|
|
340
190
|
}
|
|
341
|
-
case '
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
191
|
+
case 'persist':
|
|
192
|
+
case 'persist-key':
|
|
193
|
+
// Turned on, or another store, in the page: what the user chose before there applies
|
|
194
|
+
// now, as a host change.
|
|
195
|
+
if (this.#layoutReady && this.persist === 'local') this.#remembered = this.#recall();
|
|
196
|
+
this.#resolve('host');
|
|
345
197
|
break;
|
|
346
|
-
}
|
|
347
198
|
default:
|
|
348
|
-
|
|
199
|
+
break;
|
|
349
200
|
}
|
|
350
|
-
if (value === null) c.explicit.delete(name);
|
|
351
|
-
else c.explicit.add(name);
|
|
352
|
-
if (reflect || legacy) this.#reflect(name, value);
|
|
353
|
-
if (legacy) this.#reflect('scope', c.scope);
|
|
354
201
|
}
|
|
355
202
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
get mode(): ViewerMode {
|
|
359
|
-
return this.controller.mode;
|
|
203
|
+
protected override upgradedProperties(): readonly string[] {
|
|
204
|
+
return ['chrome', 'legend', 'panels', 'persist', 'persistKey'];
|
|
360
205
|
}
|
|
361
206
|
|
|
362
|
-
|
|
363
|
-
|
|
207
|
+
protected sheets(doc: Document): readonly CSSStyleSheet[] {
|
|
208
|
+
return [stylesheet(doc)];
|
|
364
209
|
}
|
|
365
210
|
|
|
366
|
-
|
|
367
|
-
return
|
|
211
|
+
protected createSession(host: SessionHost, deps: SessionDeps): ViewerController {
|
|
212
|
+
return new ViewerController(host, deps);
|
|
368
213
|
}
|
|
369
214
|
|
|
370
|
-
|
|
371
|
-
this.#
|
|
215
|
+
protected override keysEnabled(): boolean {
|
|
216
|
+
return this.#ui !== null && this.chrome !== 'none';
|
|
372
217
|
}
|
|
373
218
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
return this.controller.palette;
|
|
219
|
+
protected override interceptKey(e: KeyboardEvent): boolean {
|
|
220
|
+
const ui = this.#ui;
|
|
221
|
+
if (!ui || !(ui.help.open || ui.menu.open)) return false;
|
|
222
|
+
if (e.key === 'Escape') {
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
ui.help.hide();
|
|
225
|
+
this.#toggleMenu(false);
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
384
228
|
}
|
|
385
229
|
|
|
386
|
-
|
|
387
|
-
|
|
230
|
+
protected override payloadSet(payload: ReportPayload): void {
|
|
231
|
+
// Validated (and warned about) once more when it loads.
|
|
232
|
+
this.#report = sanitizeView(payload.view, true)?.panels ?? {};
|
|
233
|
+
this.#resolve('host');
|
|
388
234
|
}
|
|
389
235
|
|
|
390
|
-
|
|
391
|
-
|
|
236
|
+
override load(options: LoadOptions): Promise<void> {
|
|
237
|
+
this.#report = {};
|
|
238
|
+
this.#resolve('host');
|
|
239
|
+
return super.load(options);
|
|
392
240
|
}
|
|
393
241
|
|
|
394
|
-
|
|
395
|
-
this.#applyPreference('locale', value, true);
|
|
396
|
-
}
|
|
242
|
+
// ─── Properties ───────────────────────────────────────────────────────────
|
|
397
243
|
|
|
398
244
|
get chrome(): ViewerChrome {
|
|
399
245
|
return toChrome(this.getAttribute('chrome')) ?? 'full';
|
|
400
246
|
}
|
|
401
247
|
|
|
402
248
|
set chrome(value: ViewerChrome) {
|
|
403
|
-
|
|
249
|
+
const v = toChrome(value) === 'none' ? 'none' : null;
|
|
250
|
+
this.reflect('chrome', v);
|
|
251
|
+
this.#preset('chrome', v);
|
|
404
252
|
this.#applyChrome();
|
|
405
253
|
}
|
|
406
254
|
|
|
@@ -410,148 +258,199 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
410
258
|
|
|
411
259
|
set legend(value: ViewerLegend) {
|
|
412
260
|
const v = toLegend(value);
|
|
413
|
-
this
|
|
261
|
+
this.reflect('legend', v === 'auto' ? null : v);
|
|
414
262
|
this.#applyChrome();
|
|
415
263
|
}
|
|
416
264
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
set loop(value: boolean) {
|
|
422
|
-
this.toggleAttribute('loop', Boolean(value));
|
|
265
|
+
/** The host's declaration (the `panels` attribute as an object); `layout` has the states. */
|
|
266
|
+
get panels(): ViewerPanels {
|
|
267
|
+
return { ...this.#hostPanels };
|
|
423
268
|
}
|
|
424
269
|
|
|
425
|
-
|
|
426
|
-
|
|
270
|
+
set panels(value: ViewerPanels | string | null) {
|
|
271
|
+
const { panels, invalid } = toPanels(value);
|
|
272
|
+
warnPanels(invalid);
|
|
273
|
+
this.reflect('panels', formatPanels(panels) || null);
|
|
274
|
+
this.#setHostPanels(panels);
|
|
427
275
|
}
|
|
428
276
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (v === null) this.removeAttribute('loop-delay');
|
|
432
|
-
else this.setAttribute('loop-delay', String(v));
|
|
277
|
+
get persist(): 'local' | 'none' {
|
|
278
|
+
return this.getAttribute('persist') === 'none' ? 'none' : 'local';
|
|
433
279
|
}
|
|
434
280
|
|
|
435
|
-
|
|
436
|
-
|
|
281
|
+
set persist(value: 'local' | 'none') {
|
|
282
|
+
if (value === 'none') this.setAttribute('persist', 'none');
|
|
283
|
+
else this.removeAttribute('persist');
|
|
437
284
|
}
|
|
438
285
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
286
|
+
/** Where the layout remembers (`persist-key`): a `localStorage` key, `kgdiff:viewer` by default. */
|
|
287
|
+
get persistKey(): string {
|
|
288
|
+
return this.getAttribute('persist-key') || LAYOUT_KEY;
|
|
442
289
|
}
|
|
443
290
|
|
|
444
|
-
|
|
445
|
-
|
|
291
|
+
set persistKey(value: string) {
|
|
292
|
+
if (!value || value === LAYOUT_KEY) this.removeAttribute('persist-key');
|
|
293
|
+
else this.setAttribute('persist-key', String(value));
|
|
446
294
|
}
|
|
447
295
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
this.controller.loadPayload(payload).catch(() => {
|
|
459
|
-
// Already surfaced: error state + `kgdiff-error`.
|
|
460
|
-
});
|
|
296
|
+
/** Every region's state and the sizes now, as `kgdiff-layout` reports them. */
|
|
297
|
+
get layout(): ViewerLayout {
|
|
298
|
+
const sidebar = this.#ui?.sidebar;
|
|
299
|
+
return {
|
|
300
|
+
panels: { ...this.#states },
|
|
301
|
+
sidebarWidth: sidebar?.width ?? SIDEBAR_WIDTH,
|
|
302
|
+
detailShare: sidebar?.detailShare ?? null,
|
|
303
|
+
};
|
|
461
304
|
}
|
|
462
305
|
|
|
463
|
-
// ───
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
306
|
+
// ─── Panels ───────────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
/** Some region's state may have changed: apply the new states, and report them. */
|
|
309
|
+
#resolve(source: 'user' | 'host'): void {
|
|
310
|
+
const remembered = this.persist === 'local' ? this.#remembered.panels : {};
|
|
311
|
+
const next = resolvePanels([
|
|
312
|
+
this.#runtime,
|
|
313
|
+
this.#hostPanels,
|
|
314
|
+
this.#presets,
|
|
315
|
+
remembered,
|
|
316
|
+
this.#report,
|
|
317
|
+
]);
|
|
318
|
+
if (samePanels(next, this.#states)) return;
|
|
319
|
+
this.#states = next;
|
|
320
|
+
this.#applyPanels();
|
|
321
|
+
if (this.#layoutReady) this.#emitLayout(source);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#emitLayout(source: 'user' | 'host'): void {
|
|
325
|
+
this.dispatchEvent(
|
|
326
|
+
new CustomEvent('kgdiff-layout', {
|
|
327
|
+
detail: { ...this.layout, source },
|
|
328
|
+
bubbles: true,
|
|
329
|
+
composed: true,
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
468
332
|
}
|
|
469
333
|
|
|
470
|
-
|
|
471
|
-
|
|
334
|
+
/**
|
|
335
|
+
* The host declared `panels`. Where its value changed, what the user changed there in this
|
|
336
|
+
* page gives way; declaring the same again (a framework re-rendering, an attribute set to
|
|
337
|
+
* the value it has) leaves the user's change alone.
|
|
338
|
+
*/
|
|
339
|
+
#setHostPanels(panels: ViewerPanels): void {
|
|
340
|
+
this.#yield(this.#hostPanels, panels);
|
|
341
|
+
this.#hostPanels = panels;
|
|
342
|
+
this.#resolve('host');
|
|
472
343
|
}
|
|
473
344
|
|
|
474
|
-
|
|
475
|
-
|
|
345
|
+
/**
|
|
346
|
+
* A preset attribute changed. `commanded` (`sidebar`, which the layout reflects): the
|
|
347
|
+
* sidebar's regions yield even when the preset the page set last is the same.
|
|
348
|
+
*/
|
|
349
|
+
#preset(name: Preset, value: string | null, commanded = false): void {
|
|
350
|
+
this.#presetAttrs[name] = value;
|
|
351
|
+
const { sidebar, header, chrome } = this.#presetAttrs;
|
|
352
|
+
const next = presetPanels(sidebar, header, chrome);
|
|
353
|
+
const yielded = this.#yield(this.#presets, next);
|
|
354
|
+
if (commanded) {
|
|
355
|
+
for (const r of SIDEBAR_REGIONS) delete this.#runtime[r];
|
|
356
|
+
}
|
|
357
|
+
if (commanded || yielded.some((r) => SIDEBAR_REGIONS.includes(r))) this.#folded = null;
|
|
358
|
+
this.#presets = next;
|
|
359
|
+
this.#resolve('host');
|
|
476
360
|
}
|
|
477
361
|
|
|
478
|
-
|
|
479
|
-
|
|
362
|
+
/** Drop the user's in-page change of every region `before` and `after` disagree on. */
|
|
363
|
+
#yield(before: ViewerPanels, after: ViewerPanels): PanelRegion[] {
|
|
364
|
+
const changed = PANEL_REGIONS.filter((r) => before[r] !== after[r]);
|
|
365
|
+
for (const r of changed) delete this.#runtime[r];
|
|
366
|
+
return changed;
|
|
480
367
|
}
|
|
481
368
|
|
|
482
|
-
|
|
483
|
-
|
|
369
|
+
/** The user changed some regions: they hold in this page, and are remembered. */
|
|
370
|
+
#userPanels(changes: ViewerPanels): void {
|
|
371
|
+
Object.assign(this.#runtime, changes);
|
|
372
|
+
this.#remember({ panels: { ...this.#remembered.panels, ...changes } });
|
|
373
|
+
this.#resolve('user');
|
|
484
374
|
}
|
|
485
375
|
|
|
486
|
-
|
|
487
|
-
|
|
376
|
+
/** What the user chose before; theme, palette and language apply unless the host set them. */
|
|
377
|
+
#recall(): Remembered {
|
|
378
|
+
const r = readRemembered(this.#storage, this.persistKey);
|
|
379
|
+
// A preference the host set wins; one the user chose before wins over a report's view.
|
|
380
|
+
const c = this.controller;
|
|
381
|
+
if (r.theme && !this.hasAttribute('theme') && !c.explicit.has('theme')) {
|
|
382
|
+
c.setThemePreference(r.theme);
|
|
383
|
+
c.explicit.add('theme');
|
|
384
|
+
}
|
|
385
|
+
if (r.palette && !this.hasAttribute('palette') && !c.explicit.has('palette')) {
|
|
386
|
+
c.setPalette(r.palette);
|
|
387
|
+
c.explicit.add('palette');
|
|
388
|
+
}
|
|
389
|
+
if (r.locale && !this.hasAttribute('locale') && !c.explicit.has('locale')) {
|
|
390
|
+
c.setLocale(r.locale);
|
|
391
|
+
c.explicit.add('locale');
|
|
392
|
+
}
|
|
393
|
+
this.#ui?.sidebar.setSizes(r.sidebarWidth ?? SIDEBAR_WIDTH, r.detailShare ?? null, true);
|
|
394
|
+
return r;
|
|
488
395
|
}
|
|
489
396
|
|
|
490
|
-
|
|
491
|
-
|
|
397
|
+
/** The first connection: the remembered states are the first ones (not a change, no event). */
|
|
398
|
+
#loadRemembered(): void {
|
|
399
|
+
if (this.persist !== 'local') return;
|
|
400
|
+
this.#remembered = this.#recall();
|
|
401
|
+
this.#resolve('host');
|
|
492
402
|
}
|
|
493
403
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (
|
|
497
|
-
|
|
498
|
-
|
|
404
|
+
/** Remember the user's choice (merged into what other viewers of this origin wrote). */
|
|
405
|
+
#remember(patch: Partial<Remembered>): void {
|
|
406
|
+
if (this.persist !== 'local') return;
|
|
407
|
+
const stored = readRemembered(this.#storage, this.persistKey);
|
|
408
|
+
const next: Remembered = {
|
|
409
|
+
...stored,
|
|
410
|
+
...patch,
|
|
411
|
+
panels: { ...stored.panels, ...(patch.panels ?? {}) },
|
|
412
|
+
};
|
|
413
|
+
this.#remembered = { ...this.#remembered, ...patch, panels: next.panels };
|
|
414
|
+
writeRemembered(this.#storage, next, this.persistKey);
|
|
499
415
|
}
|
|
500
416
|
|
|
501
|
-
|
|
502
|
-
|
|
417
|
+
/** The user resized the sidebar or moved the split (once the drag or key press ended). */
|
|
418
|
+
#resized(width: number, detailShare: number | null): void {
|
|
419
|
+
this.#remember({ sidebarWidth: width, detailShare });
|
|
420
|
+
this.#emitLayout('user');
|
|
503
421
|
}
|
|
504
422
|
|
|
505
|
-
|
|
506
|
-
this
|
|
423
|
+
#applyPanels(): void {
|
|
424
|
+
const ui = this.#ui;
|
|
425
|
+
if (!ui) return;
|
|
426
|
+
const s = this.#states;
|
|
427
|
+
ui.sidebar.setPanels(s);
|
|
428
|
+
ui.stage.setPanels(s);
|
|
429
|
+
const sidebar = SIDEBAR_REGIONS.some((r) => s[r] !== 'hidden');
|
|
430
|
+
// A `<kgdiff-title>` or `<kgdiff-actions>` slotted into the viewer replaces the HUD's.
|
|
431
|
+
const title = ui.stage.replaces('title') ? 'hidden' : s.title;
|
|
432
|
+
const actions = ui.stage.replaces('actions') ? 'hidden' : s.actions;
|
|
433
|
+
ui.header.setPanels(title, actions, sidebar, () => this.#canvas());
|
|
434
|
+
ui.header.setSidebarState(this.#sidebarOpen);
|
|
435
|
+
// The top corners sit under what the HUD shows there.
|
|
436
|
+
const hud = [title === 'open' ? 'title' : '', actions === 'open' ? 'actions' : ''];
|
|
437
|
+
ui.stage.el.dataset.hud = hud.filter(Boolean).join(' ');
|
|
507
438
|
}
|
|
508
439
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
this.controller.fail(error);
|
|
440
|
+
#canvas(): HTMLElement | null {
|
|
441
|
+
return this.#ui?.stage.canvasHost.querySelector('canvas') ?? null;
|
|
512
442
|
}
|
|
513
443
|
|
|
514
444
|
// ─── Internals ────────────────────────────────────────────────────────────
|
|
515
445
|
|
|
516
|
-
|
|
517
|
-
this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
#reflect(name: string, value: string | null): void {
|
|
521
|
-
if (this.getAttribute(name) === value) return;
|
|
522
|
-
this.#reflecting = true;
|
|
523
|
-
try {
|
|
524
|
-
if (value === null) this.removeAttribute(name);
|
|
525
|
-
else this.setAttribute(name, value);
|
|
526
|
-
} finally {
|
|
527
|
-
this.#reflecting = false;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
#announce(text: string): void {
|
|
532
|
-
const live = this.#ui?.live;
|
|
533
|
-
if (!live) return;
|
|
534
|
-
live.textContent = '';
|
|
535
|
-
// A fresh text node in a later task makes screen readers announce repeats too.
|
|
536
|
-
setTimeout(() => {
|
|
537
|
-
live.textContent = text;
|
|
538
|
-
}, 30);
|
|
539
|
-
}
|
|
540
|
-
|
|
446
|
+
/** The sidebar is open while one of its regions is. */
|
|
541
447
|
get #sidebarOpen(): boolean {
|
|
542
|
-
return
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
#applySidebar(): void {
|
|
546
|
-
const ui = this.#ui;
|
|
547
|
-
if (!ui) return;
|
|
548
|
-
ui.sidebar.setOpen(this.#sidebarOpen);
|
|
549
|
-
ui.header.setSidebarState(this.#sidebarOpen);
|
|
448
|
+
return SIDEBAR_REGIONS.some((r) => this.#states[r] === 'open');
|
|
550
449
|
}
|
|
551
450
|
|
|
552
451
|
/**
|
|
553
452
|
* `chrome` and `legend`: what the stage shows, whether clicks select, whether the view stays
|
|
554
|
-
* fitted.
|
|
453
|
+
* fitted. Which regions show is the `chrome="none"` preset's (see `panels.ts`).
|
|
555
454
|
*/
|
|
556
455
|
#applyChrome(): void {
|
|
557
456
|
const bare = this.chrome === 'none';
|
|
@@ -564,7 +463,7 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
564
463
|
ui.root.dataset.chrome = this.chrome;
|
|
565
464
|
const legend = this.legend;
|
|
566
465
|
ui.stage.setChrome(legend === 'auto' ? (bare ? 'mini' : 'full') : legend, !bare);
|
|
567
|
-
const canvas =
|
|
466
|
+
const canvas = this.#canvas();
|
|
568
467
|
if (canvas) canvas.tabIndex = bare ? -1 : 0;
|
|
569
468
|
if (bare) {
|
|
570
469
|
ui.help.hide();
|
|
@@ -572,57 +471,68 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
572
471
|
}
|
|
573
472
|
}
|
|
574
473
|
|
|
575
|
-
/**
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
#build(): void {
|
|
581
|
-
const style = h('style', { text: STYLES });
|
|
582
|
-
const vars = h('style');
|
|
583
|
-
const ctx: UiContext = {
|
|
584
|
-
controller: this.controller,
|
|
585
|
-
tr: () => translator(this.controller.locale),
|
|
586
|
-
root: this.#shadow,
|
|
587
|
-
setMode: (mode) => {
|
|
588
|
-
this.controller.setMode(mode);
|
|
589
|
-
},
|
|
590
|
-
toggleScope: () => {
|
|
591
|
-
this.controller.toggleScope();
|
|
592
|
-
},
|
|
474
|
+
/** What the layout does for the regions: preferences reflect as attributes, panels open. */
|
|
475
|
+
protected actions(): UiActions {
|
|
476
|
+
return {
|
|
477
|
+
...this.baseActions(),
|
|
593
478
|
setThemePreference: (pref) => {
|
|
594
479
|
this.theme = pref;
|
|
480
|
+
this.#remember({ theme: this.theme });
|
|
595
481
|
},
|
|
596
482
|
setPalette: (p) => {
|
|
597
483
|
this.palette = p;
|
|
484
|
+
this.#remember({ palette: this.palette });
|
|
598
485
|
},
|
|
599
486
|
setLocale: (l) => {
|
|
600
487
|
this.locale = l;
|
|
488
|
+
this.#remember({ locale: this.locale });
|
|
601
489
|
},
|
|
602
490
|
toggleSidebar: (open) => this.#toggleSidebar(open),
|
|
603
491
|
sidebarOpen: () => this.#sidebarOpen,
|
|
492
|
+
hasSidebar: () => SIDEBAR_REGIONS.some((r) => this.#states[r] !== 'hidden'),
|
|
604
493
|
toggleMenu: (open) => this.#toggleMenu(open),
|
|
605
494
|
openHelp: () => this.#ui?.help.show(),
|
|
606
495
|
focusSearch: () => this.#focusSearch(),
|
|
496
|
+
setPanel: (region, state) => this.#userPanels({ [region]: state }),
|
|
607
497
|
};
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
#build(): void {
|
|
501
|
+
const root = h('div', { class: 'root' });
|
|
502
|
+
this.shadow.append(root);
|
|
503
|
+
// Each region binds to the root on its own (context protocol); the layout only places them.
|
|
504
|
+
const header = bindRegion(root, (ui) => new Header(ui));
|
|
505
|
+
const list = bindRegion(root, (ui) => new ChangeList(ui));
|
|
506
|
+
const search = bindRegion(root, (ui) => new Search(ui));
|
|
507
|
+
const filters = bindRegion(root, (ui) => new Filters(ui));
|
|
508
|
+
const detail = bindRegion(root, (ui) => new Detail(ui));
|
|
509
|
+
// The stage's slots are the viewer's: the page's content goes over the canvas.
|
|
510
|
+
const stage = bindRegion(root, (ui) => new Stage(ui, true));
|
|
511
|
+
stage.view = this.controller.view;
|
|
512
|
+
stage.onReplaced = () => this.#applyPanels();
|
|
513
|
+
this.#provideStage(stage);
|
|
514
|
+
const ui = uiContext(this.root, this.shadow);
|
|
515
|
+
const menu = new Menu(ui);
|
|
516
|
+
const help = new HelpDialog(ui);
|
|
517
|
+
const sidebar = new Sidebar(
|
|
518
|
+
ui,
|
|
519
|
+
{
|
|
520
|
+
setPanel: (region, state) => this.#userPanels({ [region]: state }),
|
|
521
|
+
resized: (width, share) => this.#resized(width, share),
|
|
522
|
+
focusOutside: () => (isOpen(header.sidebarButton) ? header.sidebarButton : this.#canvas()),
|
|
523
|
+
},
|
|
524
|
+
list,
|
|
525
|
+
search,
|
|
526
|
+
filters,
|
|
527
|
+
detail,
|
|
528
|
+
);
|
|
614
529
|
// The title and the actions float over the canvas; the menu drops from the actions.
|
|
615
530
|
header.attachMenu(menu.el);
|
|
616
531
|
stage.el.appendChild(header.el);
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
help.el,
|
|
622
|
-
live,
|
|
623
|
-
);
|
|
624
|
-
this.#shadow.append(style, vars, root);
|
|
625
|
-
this.#ui = { root, vars, header, stage, sidebar, menu, help, live };
|
|
532
|
+
root.append(h('div', { class: 'body' }, stage.el, sidebar.el), help.el, this.root.live);
|
|
533
|
+
// Content slotted before the layout was built.
|
|
534
|
+
stage.slotted();
|
|
535
|
+
this.#ui = { root, header, stage, list, sidebar, menu, help };
|
|
626
536
|
// Close the menu on outside clicks.
|
|
627
537
|
root.addEventListener('pointerdown', (e) => {
|
|
628
538
|
if (!menu.open) return;
|
|
@@ -631,69 +541,75 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
631
541
|
});
|
|
632
542
|
const hover = (over: boolean) => (e: PointerEvent) => {
|
|
633
543
|
if (e.pointerType === 'touch') return;
|
|
634
|
-
this
|
|
635
|
-
this.#applyHold();
|
|
544
|
+
this.pointerOver(over);
|
|
636
545
|
};
|
|
637
546
|
stage.el.addEventListener('pointerenter', hover(true));
|
|
638
547
|
stage.el.addEventListener('pointerleave', hover(false));
|
|
639
|
-
|
|
548
|
+
sidebar.setSizes(SIDEBAR_WIDTH, null, true);
|
|
549
|
+
this.#applyPanels();
|
|
640
550
|
this.#applyChrome();
|
|
641
551
|
}
|
|
642
552
|
|
|
643
|
-
/**
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
this
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
553
|
+
/**
|
|
554
|
+
* The viewer is the stage of the chrome that acts on a view (`<kgdiff-zoom>`,
|
|
555
|
+
* `<kgdiff-minimap>`) inside it or naming it with `stage`: its built-in stage's view.
|
|
556
|
+
*/
|
|
557
|
+
#provideStage(stage: Stage): void {
|
|
558
|
+
const view = this.controller.view;
|
|
559
|
+
const ctx: StageViewContext = {
|
|
560
|
+
stage: this,
|
|
561
|
+
root: this.root,
|
|
562
|
+
view,
|
|
563
|
+
registerObstacle: (el) => stage.registerObstacle(el),
|
|
564
|
+
watchView: (listener) => {
|
|
565
|
+
listener(view);
|
|
566
|
+
return () => undefined;
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
provideContext(this, stageContext, () => ctx);
|
|
658
570
|
}
|
|
659
571
|
|
|
660
|
-
|
|
572
|
+
/** The layout's part of a render (the root renders the regions after it). */
|
|
573
|
+
#update(topics: ReadonlySet<Topic>): void {
|
|
661
574
|
const ui = this.#ui;
|
|
662
575
|
const c = this.controller;
|
|
663
|
-
|
|
664
|
-
this.#reflect('mode', c.mode);
|
|
665
|
-
this.#reflect('scope', c.scope);
|
|
666
|
-
}
|
|
667
|
-
if (topics.has('theme')) {
|
|
668
|
-
this.#reflect(
|
|
669
|
-
'theme',
|
|
670
|
-
c.themePreference === 'auto' && !this.hasAttribute('theme') ? null : c.themePreference,
|
|
671
|
-
);
|
|
672
|
-
}
|
|
576
|
+
this.reflectSession(topics);
|
|
673
577
|
if (!ui) return;
|
|
674
|
-
if (topics.has('theme') || topics.has('status'))
|
|
675
|
-
ui.vars.textContent = `:host { ${themeVariables(c.theme)} }`;
|
|
676
|
-
ui.root.dataset.theme = c.themeName;
|
|
677
|
-
}
|
|
578
|
+
if (topics.has('theme') || topics.has('status')) ui.root.dataset.theme = c.themeName;
|
|
678
579
|
if (topics.has('locale') || topics.has('status')) {
|
|
679
580
|
ui.root.setAttribute('lang', c.locale);
|
|
680
581
|
if (ui.menu.open) ui.menu.render();
|
|
681
582
|
if (ui.help.open) ui.help.render();
|
|
682
583
|
}
|
|
683
584
|
if (topics.has('theme') && ui.menu.open) ui.menu.render();
|
|
684
|
-
ui.header.update(topics);
|
|
685
585
|
ui.sidebar.update(topics);
|
|
686
|
-
ui.stage.update(topics);
|
|
687
586
|
if (topics.has('mode') || topics.has('status')) {
|
|
688
587
|
ui.root.dataset.mode = c.mode;
|
|
689
588
|
ui.root.dataset.scope = c.scope;
|
|
690
589
|
}
|
|
691
590
|
}
|
|
692
591
|
|
|
592
|
+
/**
|
|
593
|
+
* Fold the sidebar (its open regions collapse) or open it again (the regions that were open,
|
|
594
|
+
* or all four). Reflected to the `sidebar` attribute.
|
|
595
|
+
*/
|
|
693
596
|
#toggleSidebar(open?: boolean): void {
|
|
597
|
+
const s = this.#states;
|
|
598
|
+
const shown = SIDEBAR_REGIONS.filter((r) => s[r] !== 'hidden');
|
|
599
|
+
if (shown.length === 0) return;
|
|
694
600
|
const next = open ?? !this.#sidebarOpen;
|
|
695
|
-
this
|
|
696
|
-
this.#
|
|
601
|
+
this.reflect('sidebar', next ? 'open' : 'closed');
|
|
602
|
+
if (next === this.#sidebarOpen) return;
|
|
603
|
+
const changes: MutablePanels = {};
|
|
604
|
+
if (next) {
|
|
605
|
+
const restore = this.#folded?.filter((r) => shown.includes(r)) ?? [];
|
|
606
|
+
for (const r of restore.length > 0 ? restore : shown) changes[r] = 'open';
|
|
607
|
+
this.#folded = null;
|
|
608
|
+
} else {
|
|
609
|
+
this.#folded = shown.filter((r) => s[r] === 'open');
|
|
610
|
+
for (const r of shown) changes[r] = 'collapsed';
|
|
611
|
+
}
|
|
612
|
+
this.#userPanels(changes);
|
|
697
613
|
}
|
|
698
614
|
|
|
699
615
|
#toggleMenu(open?: boolean): void {
|
|
@@ -705,24 +621,13 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
705
621
|
ui.header.setMenuState(next);
|
|
706
622
|
}
|
|
707
623
|
|
|
624
|
+
/** `/`: an open search takes focus; else this layout's search opens, unless it is hidden. */
|
|
708
625
|
#focusSearch(): void {
|
|
626
|
+
if (this.root.focus('search')) return;
|
|
627
|
+
if (this.#states.search === 'hidden') return;
|
|
709
628
|
if (!this.#sidebarOpen) this.#toggleSidebar(true);
|
|
710
|
-
this.#
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
#watchMedia(): void {
|
|
714
|
-
const dark = media('(prefers-color-scheme: light)');
|
|
715
|
-
const motion = media('(prefers-reduced-motion: reduce)');
|
|
716
|
-
const apply = (): void => {
|
|
717
|
-
this.controller.setSystemDark(!(dark?.matches ?? false));
|
|
718
|
-
this.controller.setReducedMotion(motion?.matches ?? false);
|
|
719
|
-
};
|
|
720
|
-
apply();
|
|
721
|
-
for (const q of [dark, motion]) {
|
|
722
|
-
if (!q) continue;
|
|
723
|
-
q.addEventListener?.('change', apply);
|
|
724
|
-
this.#cleanup.push(() => q.removeEventListener?.('change', apply));
|
|
725
|
-
}
|
|
629
|
+
if (this.#states.search === 'collapsed') this.#userPanels({ search: 'open' });
|
|
630
|
+
this.root.focus('search');
|
|
726
631
|
}
|
|
727
632
|
|
|
728
633
|
#watchSize(): void {
|
|
@@ -738,8 +643,8 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
738
643
|
ui.root.dataset.compact = String(compact);
|
|
739
644
|
}
|
|
740
645
|
ui.stage.layout();
|
|
741
|
-
ui.
|
|
742
|
-
|
|
646
|
+
ui.list.renderWindow();
|
|
647
|
+
ui.sidebar.layout(compact);
|
|
743
648
|
this.controller.layoutChanged();
|
|
744
649
|
});
|
|
745
650
|
ro.observe(this);
|
|
@@ -747,142 +652,4 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
|
|
|
747
652
|
if (this.#ui) ro.observe(this.#ui.stage.el);
|
|
748
653
|
this.#cleanup.push(() => ro.disconnect());
|
|
749
654
|
}
|
|
750
|
-
|
|
751
|
-
#onKeyDown(e: KeyboardEvent): void {
|
|
752
|
-
if (e.defaultPrevented || e.isComposing) return;
|
|
753
|
-
const ui = this.#ui;
|
|
754
|
-
if (!ui || this.chrome === 'none') return;
|
|
755
|
-
const path = e.composedPath();
|
|
756
|
-
if (!path.includes(this)) {
|
|
757
|
-
// Outside the component only with `global-shortcuts`, when nothing else has focus, for
|
|
758
|
-
// the first such viewer, and never the keys pages scroll with.
|
|
759
|
-
if (!this.hasAttribute('global-shortcuts') || PAGE_KEYS.has(e.key)) return;
|
|
760
|
-
const active = document.activeElement;
|
|
761
|
-
if (active && active !== document.body && active !== document.documentElement) return;
|
|
762
|
-
if (connected.find((v) => v.hasAttribute('global-shortcuts')) !== this) return;
|
|
763
|
-
}
|
|
764
|
-
if (ui.help.open || ui.menu.open) {
|
|
765
|
-
if (e.key === 'Escape') {
|
|
766
|
-
e.preventDefault();
|
|
767
|
-
ui.help.hide();
|
|
768
|
-
this.#toggleMenu(false);
|
|
769
|
-
}
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
const target = path[0] ?? null;
|
|
773
|
-
if (isTypingTarget(target)) return;
|
|
774
|
-
const c = this.controller;
|
|
775
|
-
const action = keyAction(e, c.mode, c.scope);
|
|
776
|
-
if (!action) return;
|
|
777
|
-
// Enter / Space on a focused control activate that control.
|
|
778
|
-
const el = target as Element | null;
|
|
779
|
-
if (
|
|
780
|
-
(e.key === 'Enter' || e.key === ' ') &&
|
|
781
|
-
el?.closest?.('button, a, summary, [role="radio"], [role="menuitemradio"]')
|
|
782
|
-
) {
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
if (c.status !== 'ready' && !['help', 'theme', 'sidebar', 'escape'].includes(action.type))
|
|
786
|
-
return;
|
|
787
|
-
e.preventDefault();
|
|
788
|
-
switch (action.type) {
|
|
789
|
-
case 'mode':
|
|
790
|
-
c.setMode(action.mode);
|
|
791
|
-
break;
|
|
792
|
-
case 'scope':
|
|
793
|
-
c.toggleScope();
|
|
794
|
-
break;
|
|
795
|
-
case 'next':
|
|
796
|
-
c.next();
|
|
797
|
-
break;
|
|
798
|
-
case 'prev':
|
|
799
|
-
c.prev();
|
|
800
|
-
break;
|
|
801
|
-
case 'first':
|
|
802
|
-
c.first();
|
|
803
|
-
break;
|
|
804
|
-
case 'last':
|
|
805
|
-
c.last();
|
|
806
|
-
break;
|
|
807
|
-
case 'fly':
|
|
808
|
-
c.flyToSelection();
|
|
809
|
-
break;
|
|
810
|
-
case 'escape':
|
|
811
|
-
if (!ui.sidebar.escape()) c.clearSelection();
|
|
812
|
-
break;
|
|
813
|
-
case 'play-pause':
|
|
814
|
-
c.togglePlay();
|
|
815
|
-
break;
|
|
816
|
-
case 'step':
|
|
817
|
-
c.stepReplay(action.direction);
|
|
818
|
-
break;
|
|
819
|
-
case 'divider':
|
|
820
|
-
c.setSplit(c.split + action.delta);
|
|
821
|
-
break;
|
|
822
|
-
case 'fit':
|
|
823
|
-
void c.fit();
|
|
824
|
-
break;
|
|
825
|
-
case 'zoom':
|
|
826
|
-
c.zoom(action.direction);
|
|
827
|
-
break;
|
|
828
|
-
case 'search':
|
|
829
|
-
this.#focusSearch();
|
|
830
|
-
break;
|
|
831
|
-
case 'review':
|
|
832
|
-
c.toggleReviewed();
|
|
833
|
-
break;
|
|
834
|
-
case 'sidebar':
|
|
835
|
-
this.#toggleSidebar();
|
|
836
|
-
break;
|
|
837
|
-
case 'theme': {
|
|
838
|
-
const i = THEME_PREFERENCES.indexOf(c.themePreference);
|
|
839
|
-
this.theme = THEME_PREFERENCES[(i + 1) % THEME_PREFERENCES.length] as ThemePreference;
|
|
840
|
-
break;
|
|
841
|
-
}
|
|
842
|
-
case 'help':
|
|
843
|
-
ui.help.show();
|
|
844
|
-
break;
|
|
845
|
-
case 'expand':
|
|
846
|
-
c.expandNearest();
|
|
847
|
-
break;
|
|
848
|
-
default:
|
|
849
|
-
action satisfies never;
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// ─── Deep links ───────────────────────────────────────────────────────────
|
|
854
|
-
|
|
855
|
-
#readHash(): void {
|
|
856
|
-
if (!this.hasAttribute('sync-hash') || typeof location === 'undefined') return;
|
|
857
|
-
if (location.hash === this.#lastHash) return;
|
|
858
|
-
const state = parseHash(location.hash);
|
|
859
|
-
this.#lastHash = location.hash;
|
|
860
|
-
if (!state) return;
|
|
861
|
-
if (state.mode || state.scope) {
|
|
862
|
-
// A deep link outranks the large-graph default and a report's preferred view.
|
|
863
|
-
const c = this.controller;
|
|
864
|
-
if (state.mode) c.explicit.add('mode');
|
|
865
|
-
if (state.scope) c.explicit.add('scope');
|
|
866
|
-
c.setView(state.mode ?? c.mode, state.scope ?? c.scope);
|
|
867
|
-
}
|
|
868
|
-
if (state.hunk) this.controller.selectHunkById(state.hunk, { fly: true });
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
#writeHash(): void {
|
|
872
|
-
if (!this.hasAttribute('sync-hash') || typeof location === 'undefined') return;
|
|
873
|
-
const c = this.controller;
|
|
874
|
-
const hunk = c.selectedHunk >= 0 ? c.scene?.hunks[c.selectedHunk]?.id : undefined;
|
|
875
|
-
const hash = formatHash({
|
|
876
|
-
...(hunk !== undefined ? { hunk } : {}),
|
|
877
|
-
mode: c.mode,
|
|
878
|
-
scope: c.scope,
|
|
879
|
-
});
|
|
880
|
-
if (hash === location.hash) return;
|
|
881
|
-
this.#lastHash = hash;
|
|
882
|
-
try {
|
|
883
|
-
history.replaceState(history.state, '', hash);
|
|
884
|
-
} catch {
|
|
885
|
-
location.hash = hash;
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
655
|
}
|