@kgdiff/viewer 0.2.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 +48 -6
- package/dist/controller.d.ts +63 -14
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +308 -54
- package/dist/controller.js.map +1 -1
- package/dist/element.d.ts +17 -4
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +199 -26
- package/dist/element.js.map +1 -1
- package/dist/framing.d.ts +20 -1
- package/dist/framing.d.ts.map +1 -1
- package/dist/framing.js +49 -0
- package/dist/framing.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 +66 -6
- 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 +13 -1
- package/dist/prefs.d.ts.map +1 -1
- package/dist/prefs.js +30 -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 +84 -61
- 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 +8 -0
- package/dist/ui/stage.d.ts.map +1 -1
- package/dist/ui/stage.js +66 -93
- 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 +332 -56
- package/src/element.ts +220 -29
- package/src/framing.ts +65 -1
- package/src/hash-sync.ts +20 -6
- package/src/i18n.ts +15 -14
- package/src/index.ts +65 -7
- package/src/keyboard.ts +12 -6
- package/src/prefs.ts +45 -5
- package/src/styles.ts +84 -61
- 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 +88 -112
- package/src/ui/swatch.ts +31 -0
package/src/index.ts
CHANGED
|
@@ -25,8 +25,15 @@ import { KgdiffViewer } from './element.js';
|
|
|
25
25
|
import { hydrate } from './payload.js';
|
|
26
26
|
import { viewerTags } from './registry.js';
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
/** How the two versions are told apart: by color (overlay), side by side (swipe) or over time (replay). */
|
|
29
|
+
export type ViewerMode = 'overlay' | 'swipe' | 'replay';
|
|
30
|
+
/** What is drawn, in any mode: the whole graph, or only the changes with a few hops of context. */
|
|
31
|
+
export type ViewerScope = 'all' | 'changes';
|
|
29
32
|
export type ThemePreference = 'auto' | 'light' | 'dark';
|
|
33
|
+
/** `none`: the graph alone, for small embeds (see `KgdiffViewerElement.chrome`). */
|
|
34
|
+
export type ViewerChrome = 'full' | 'none';
|
|
35
|
+
/** The legend card, one line of dots, no legend, or `auto` (the card, one line without chrome). */
|
|
36
|
+
export type ViewerLegend = 'auto' | 'full' | 'mini' | 'none';
|
|
30
37
|
|
|
31
38
|
/** A precomputed diff + layout: what the CLI embeds in HTML reports. Deterministic. */
|
|
32
39
|
export interface ReportPayload {
|
|
@@ -45,7 +52,9 @@ export interface ReportPayload {
|
|
|
45
52
|
};
|
|
46
53
|
/** Initial view preferences. */
|
|
47
54
|
readonly view?: {
|
|
55
|
+
/** `changes` (from reports made before scopes existed) means `overlay` + scope `changes`. */
|
|
48
56
|
readonly mode?: ViewerMode;
|
|
57
|
+
readonly scope?: ViewerScope;
|
|
49
58
|
readonly theme?: ThemePreference;
|
|
50
59
|
readonly palette?: PaletteName;
|
|
51
60
|
readonly locale?: Locale;
|
|
@@ -76,36 +85,84 @@ export interface ReviewState {
|
|
|
76
85
|
export interface KgdiffViewerEventMap {
|
|
77
86
|
'kgdiff-ready': CustomEvent<{ readonly stats: DiffStats }>;
|
|
78
87
|
'kgdiff-select': CustomEvent<{ readonly hunk: string | null; readonly node: string | null }>;
|
|
79
|
-
'kgdiff-mode': CustomEvent<{ readonly mode: ViewerMode }>;
|
|
88
|
+
'kgdiff-mode': CustomEvent<{ readonly mode: ViewerMode; readonly scope: ViewerScope }>;
|
|
80
89
|
'kgdiff-review': CustomEvent<{
|
|
81
90
|
readonly hunk: string;
|
|
82
91
|
readonly reviewed: boolean;
|
|
83
92
|
readonly progress: ReviewState;
|
|
84
93
|
}>;
|
|
85
94
|
'kgdiff-error': CustomEvent<{ readonly error: Error }>;
|
|
95
|
+
/**
|
|
96
|
+
* Playback reached the end of the replay (not fired for seeking or stepping there). `loop`:
|
|
97
|
+
* it starts again after `loopDelay` ms; set `loop = false` or call `pause()` to stop it.
|
|
98
|
+
*/
|
|
99
|
+
'kgdiff-replay-end': CustomEvent<{ readonly loop: boolean }>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface FitOptions {
|
|
103
|
+
/** Default true (false with reduced motion). */
|
|
104
|
+
readonly animate?: boolean;
|
|
105
|
+
/** Room kept around the graph and its labels, px. Default 24; later automatic fits keep it. */
|
|
106
|
+
readonly padding?: number;
|
|
86
107
|
}
|
|
87
108
|
|
|
88
109
|
/**
|
|
89
110
|
* Public surface of the `<kgdiff-viewer>` element.
|
|
90
111
|
*
|
|
91
|
-
* Boolean attributes: `sync-hash` (deep links `#kgdiff=hunk:<id>;mode:<mode>`) and
|
|
112
|
+
* Boolean attributes: `sync-hash` (deep links `#kgdiff=hunk:<id>;mode:<mode>;scope:<scope>`) and
|
|
92
113
|
* `global-shortcuts`: keyboard shortcuts normally apply only while focus is inside the viewer;
|
|
93
114
|
* with this attribute they also apply when nothing on the page has focus (for full-page
|
|
94
115
|
* viewers such as HTML reports), except the keys pages scroll with (arrows, Home / End, Page
|
|
95
116
|
* Up / Down, Space).
|
|
96
117
|
*
|
|
97
|
-
* `header="compact"` leaves the brand and the compared names out of the
|
|
118
|
+
* `header="compact"` leaves the brand and the compared names out of the canvas, for hosts that
|
|
98
119
|
* show their own (such as the playground).
|
|
120
|
+
*
|
|
121
|
+
* A small looping replay for another page: `mode="replay" chrome="none" loop` (with a height of
|
|
122
|
+
* your choosing, such as 320px).
|
|
99
123
|
*/
|
|
100
124
|
export interface KgdiffViewerElement extends HTMLElement {
|
|
101
|
-
/** Reflected to the `mode` attribute. Default `overlay
|
|
102
|
-
mode: ViewerMode;
|
|
125
|
+
/** Reflected to the `mode` attribute. Default `overlay`. */
|
|
126
|
+
get mode(): ViewerMode;
|
|
127
|
+
/** The legacy `changes` mode sets `overlay` + scope `changes`. */
|
|
128
|
+
set mode(value: ViewerMode | 'changes');
|
|
129
|
+
/**
|
|
130
|
+
* Reflected to the `scope` attribute. Default `all` (`changes` for large graphs). Fixed to
|
|
131
|
+
* `changes` when the layout covers only the changes.
|
|
132
|
+
*/
|
|
133
|
+
scope: ViewerScope;
|
|
103
134
|
/** Reflected to the `theme` attribute. Default `auto`. */
|
|
104
135
|
theme: ThemePreference;
|
|
105
136
|
/** Reflected to the `palette` attribute. Default `default`. */
|
|
106
137
|
palette: PaletteName;
|
|
107
138
|
/** Reflected to the `locale` attribute. Default from `navigator.language`. */
|
|
108
139
|
locale: Locale;
|
|
140
|
+
/**
|
|
141
|
+
* Reflected to the `chrome` attribute. `none` shows the graph alone: no title, actions, view
|
|
142
|
+
* dock (with the step caption and playback bar), change list, zoom buttons, minimap or legend
|
|
143
|
+
* card; the hover tooltip and a one-line legend stay. Clicks and keyboard shortcuts do nothing, the minimum height drops
|
|
144
|
+
* from 420px to 120px, and the view stays fitted: when the canvas is resized (unless the user
|
|
145
|
+
* panned or zoomed since) and at the start of every loop. Labels are never cut at the edge:
|
|
146
|
+
* one pushed from its spot by a neighbour moves inside or is left out. Default `full`.
|
|
147
|
+
*/
|
|
148
|
+
chrome: ViewerChrome;
|
|
149
|
+
/**
|
|
150
|
+
* Reflected to the `legend` attribute: `full` (the card), `mini` (one line: the families this
|
|
151
|
+
* diff has, then unchanged), `none`. Default `auto`: `full`, or `mini` with `chrome="none"`.
|
|
152
|
+
*/
|
|
153
|
+
legend: ViewerLegend;
|
|
154
|
+
/**
|
|
155
|
+
* Reflected to the boolean `loop` attribute. In `replay` mode the replay plays by itself and
|
|
156
|
+
* starts again `loopDelay` ms after it ends. `pause()` stops it until `play()`. Default false.
|
|
157
|
+
*/
|
|
158
|
+
loop: boolean;
|
|
159
|
+
/** Reflected to `loop-delay`: ms between the end of a looping replay and its restart. Default 1200. */
|
|
160
|
+
loopDelay: number;
|
|
161
|
+
/**
|
|
162
|
+
* With `loop`, the replay pauses while the pointer is over the graph and continues when it
|
|
163
|
+
* leaves (a mouse or pen; not touch). Reflected as `pause-on-hover="false"` when off. Default true.
|
|
164
|
+
*/
|
|
165
|
+
pauseOnHover: boolean;
|
|
109
166
|
/** Set a precomputed report (no diff / layout work in the browser). */
|
|
110
167
|
payload: ReportPayload | null;
|
|
111
168
|
/** Parse (if needed), diff, lay out and show two graphs. Rejects with a KgdiffError on bad input. */
|
|
@@ -116,7 +173,8 @@ export interface KgdiffViewerElement extends HTMLElement {
|
|
|
116
173
|
prev(): void;
|
|
117
174
|
play(): void;
|
|
118
175
|
pause(): void;
|
|
119
|
-
|
|
176
|
+
/** Fit the whole graph, labels included, into view. */
|
|
177
|
+
fit(options?: FitOptions): Promise<void>;
|
|
120
178
|
/** The full delta of the current diff. */
|
|
121
179
|
exportDelta(): Delta;
|
|
122
180
|
reviewState(): ReviewState;
|
package/src/keyboard.ts
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* The keyboard map (docs/interaction.md §Keyboard map), as a pure function from a key event to
|
|
3
3
|
* an action, so it can be tested without a DOM.
|
|
4
4
|
*/
|
|
5
|
-
import type { ViewerMode } from './index.js';
|
|
5
|
+
import type { ViewerMode, ViewerScope } from './index.js';
|
|
6
6
|
|
|
7
7
|
export type KeyAction =
|
|
8
8
|
| { readonly type: 'mode'; readonly mode: ViewerMode }
|
|
9
|
+
| { readonly type: 'scope' }
|
|
9
10
|
| { readonly type: 'next' }
|
|
10
11
|
| { readonly type: 'prev' }
|
|
11
12
|
| { readonly type: 'first' }
|
|
@@ -36,7 +37,6 @@ const MODE_KEYS: Readonly<Record<string, ViewerMode>> = {
|
|
|
36
37
|
'1': 'overlay',
|
|
37
38
|
'2': 'swipe',
|
|
38
39
|
'3': 'replay',
|
|
39
|
-
'4': 'changes',
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
/** Divider steps for swipe, as a fraction of the width. */
|
|
@@ -44,15 +44,21 @@ export const DIVIDER_STEP = 0.02;
|
|
|
44
44
|
export const DIVIDER_STEP_LARGE = 0.1;
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* Map a key press to an action in `mode`, or null when the key means nothing here.
|
|
48
|
-
* Ctrl / Meta / Alt are never handled (they belong to the browser and the host page).
|
|
47
|
+
* Map a key press to an action in `mode` and `scope`, or null when the key means nothing here.
|
|
48
|
+
* Chords with Ctrl / Meta / Alt are never handled (they belong to the browser and the host page).
|
|
49
49
|
*/
|
|
50
|
-
export function keyAction(
|
|
50
|
+
export function keyAction(
|
|
51
|
+
e: KeyLike,
|
|
52
|
+
mode: ViewerMode,
|
|
53
|
+
scope: ViewerScope = 'all',
|
|
54
|
+
): KeyAction | null {
|
|
51
55
|
if (e.ctrlKey || e.metaKey || e.altKey) return null;
|
|
52
56
|
const key = e.key;
|
|
53
57
|
const mapped = MODE_KEYS[key];
|
|
54
58
|
if (mapped !== undefined) return { type: 'mode', mode: mapped };
|
|
55
59
|
switch (key) {
|
|
60
|
+
case '4':
|
|
61
|
+
return { type: 'scope' };
|
|
56
62
|
case 'j':
|
|
57
63
|
case 'ArrowDown':
|
|
58
64
|
return { type: 'next' };
|
|
@@ -104,7 +110,7 @@ export function keyAction(e: KeyLike, mode: ViewerMode): KeyAction | null {
|
|
|
104
110
|
return { type: 'theme' };
|
|
105
111
|
case 'e':
|
|
106
112
|
case 'E':
|
|
107
|
-
return
|
|
113
|
+
return scope === 'changes' ? { type: 'expand' } : null;
|
|
108
114
|
case '?':
|
|
109
115
|
return { type: 'help' };
|
|
110
116
|
default:
|
package/src/prefs.ts
CHANGED
|
@@ -6,11 +6,24 @@
|
|
|
6
6
|
import type { Locale } from '@kgdiff/core';
|
|
7
7
|
import type { PaletteName } from '@kgdiff/scene';
|
|
8
8
|
import { normalizeLocale } from './i18n.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
ReportPayload,
|
|
11
|
+
ThemePreference,
|
|
12
|
+
ViewerChrome,
|
|
13
|
+
ViewerLegend,
|
|
14
|
+
ViewerMode,
|
|
15
|
+
ViewerScope,
|
|
16
|
+
} from './index.js';
|
|
10
17
|
|
|
11
|
-
const MODES: readonly ViewerMode[] = ['overlay', 'swipe', 'replay'
|
|
18
|
+
const MODES: readonly ViewerMode[] = ['overlay', 'swipe', 'replay'];
|
|
19
|
+
const SCOPES: readonly ViewerScope[] = ['all', 'changes'];
|
|
12
20
|
const THEMES: readonly ThemePreference[] = ['auto', 'light', 'dark'];
|
|
13
21
|
const PALETTES: readonly PaletteName[] = ['default', 'colorblind'];
|
|
22
|
+
const CHROMES: readonly ViewerChrome[] = ['full', 'none'];
|
|
23
|
+
const LEGENDS: readonly ViewerLegend[] = ['auto', 'full', 'mini', 'none'];
|
|
24
|
+
|
|
25
|
+
/** Pause between the end of a looping replay and its next start, ms. */
|
|
26
|
+
export const DEFAULT_LOOP_DELAY = 1200;
|
|
14
27
|
|
|
15
28
|
export const THEME_PREFERENCES = THEMES;
|
|
16
29
|
|
|
@@ -18,6 +31,16 @@ export function toMode(value: unknown): ViewerMode | null {
|
|
|
18
31
|
return MODES.includes(value as ViewerMode) ? (value as ViewerMode) : null;
|
|
19
32
|
}
|
|
20
33
|
|
|
34
|
+
export function toScope(value: unknown): ViewerScope | null {
|
|
35
|
+
return SCOPES.includes(value as ViewerScope) ? (value as ViewerScope) : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Before scopes existed, “changes only” was a fourth mode. Attributes, properties, deep links
|
|
40
|
+
* and reports may still say `changes`: it means overlay + the `changes` scope.
|
|
41
|
+
*/
|
|
42
|
+
export const LEGACY_CHANGES_MODE = 'changes';
|
|
43
|
+
|
|
21
44
|
export function toTheme(value: unknown): ThemePreference | null {
|
|
22
45
|
return THEMES.includes(value as ThemePreference) ? (value as ThemePreference) : null;
|
|
23
46
|
}
|
|
@@ -30,24 +53,41 @@ export function toLocale(value: unknown): Locale | null {
|
|
|
30
53
|
return typeof value === 'string' ? normalizeLocale(value) : null;
|
|
31
54
|
}
|
|
32
55
|
|
|
56
|
+
export function toChrome(value: unknown): ViewerChrome | null {
|
|
57
|
+
return CHROMES.includes(value as ViewerChrome) ? (value as ViewerChrome) : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function toLegend(value: unknown): ViewerLegend | null {
|
|
61
|
+
return LEGENDS.includes(value as ViewerLegend) ? (value as ViewerLegend) : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A delay in ms (number or numeric string), at least 0. */
|
|
65
|
+
export function toDelay(value: unknown): number | null {
|
|
66
|
+
const n = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
|
|
67
|
+
return typeof n === 'number' && Number.isFinite(n) && n >= 0 ? n : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
33
70
|
type View = NonNullable<ReportPayload['view']>;
|
|
34
71
|
|
|
35
72
|
/** Keep only the valid fields of a report's `view` (warning about the rest). */
|
|
36
73
|
export function sanitizeView(view: unknown): View | undefined {
|
|
37
74
|
if (typeof view !== 'object' || view === null) return undefined;
|
|
38
75
|
const v = view as Record<string, unknown>;
|
|
39
|
-
const
|
|
76
|
+
const legacy = v.mode === LEGACY_CHANGES_MODE;
|
|
77
|
+
const mode = legacy ? 'overlay' : toMode(v.mode);
|
|
78
|
+
const scope = toScope(v.scope) ?? (legacy && v.scope === undefined ? 'changes' : null);
|
|
40
79
|
const theme = toTheme(v.theme);
|
|
41
80
|
const palette = toPalette(v.palette);
|
|
42
81
|
const locale = toLocale(v.locale);
|
|
43
|
-
const dropped = (['mode', 'theme', 'palette', 'locale'] as const).filter(
|
|
44
|
-
(k) => v[k] !== undefined && { mode, theme, palette, locale }[k] === null,
|
|
82
|
+
const dropped = (['mode', 'scope', 'theme', 'palette', 'locale'] as const).filter(
|
|
83
|
+
(k) => v[k] !== undefined && { mode, scope, theme, palette, locale }[k] === null,
|
|
45
84
|
);
|
|
46
85
|
if (dropped.length > 0 && typeof console !== 'undefined') {
|
|
47
86
|
console.warn(`[kgdiff] ignoring invalid report view preferences: ${dropped.join(', ')}`);
|
|
48
87
|
}
|
|
49
88
|
return {
|
|
50
89
|
...(mode ? { mode } : {}),
|
|
90
|
+
...(scope ? { scope } : {}),
|
|
51
91
|
...(theme ? { theme } : {}),
|
|
52
92
|
...(palette ? { palette } : {}),
|
|
53
93
|
...(locale ? { locale } : {}),
|
package/src/styles.ts
CHANGED
|
@@ -90,6 +90,8 @@ export const STYLES = /* css */ `
|
|
|
90
90
|
-webkit-font-smoothing: antialiased;
|
|
91
91
|
}
|
|
92
92
|
:host([hidden]) { display: none; }
|
|
93
|
+
/* chrome="none" shows the graph alone: small embeds (a 320 px strip) are fine. */
|
|
94
|
+
:host([chrome="none"]) { min-height: 120px; }
|
|
93
95
|
*, *::before, *::after { box-sizing: border-box; }
|
|
94
96
|
[hidden] { display: none !important; }
|
|
95
97
|
button, input { font: inherit; color: inherit; letter-spacing: inherit; }
|
|
@@ -119,7 +121,7 @@ kbd {
|
|
|
119
121
|
background: var(--kg-bg);
|
|
120
122
|
}
|
|
121
123
|
.body { position: relative; flex: 1; min-height: 0; display: flex; }
|
|
122
|
-
.stage-wrap { position: relative; flex: 1; min-width: 0; min-height: 0; overflow: hidden; background: var(--kg-bg); }
|
|
124
|
+
.stage-wrap { position: relative; flex: 1; min-width: 0; min-height: 0; overflow: hidden; background: var(--kg-bg); container-type: inline-size; }
|
|
123
125
|
.stage { position: absolute; inset: 0; }
|
|
124
126
|
.stage canvas { outline: none; }
|
|
125
127
|
.stage canvas:focus-visible { outline: 2px solid var(--kg-accent); outline-offset: -2px; }
|
|
@@ -146,17 +148,22 @@ kbd {
|
|
|
146
148
|
.link-btn { min-height: 28px; color: var(--kg-accent-text); font-weight: 500; font-size: 12px; border-radius: 4px; }
|
|
147
149
|
.link-btn:hover { text-decoration: underline; text-underline-offset: 3px; }
|
|
148
150
|
|
|
149
|
-
/* ──
|
|
150
|
-
.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
/* ── HUD: title (top-left) and actions (top-right), over the canvas ──────── */
|
|
152
|
+
.hud { position: absolute; inset: 0; z-index: 3; pointer-events: none; }
|
|
153
|
+
.hud > * { pointer-events: auto; }
|
|
154
|
+
.hud-title {
|
|
155
|
+
position: absolute; top: 12px; left: 12px; max-width: calc(100% - 184px); height: 40px;
|
|
156
|
+
display: flex; align-items: center; gap: 10px; padding: 0 14px 0 10px;
|
|
157
|
+
}
|
|
158
|
+
.hud[data-titled="false"] .header-sep { display: none; }
|
|
159
|
+
:host([header="compact"]) .hud-title { display: none; }
|
|
160
|
+
.hud-end { position: absolute; top: 12px; right: 12px; }
|
|
161
|
+
.hud-actions { display: flex; align-items: center; gap: 2px; padding: 4px; }
|
|
154
162
|
.brand { display: flex; align-items: center; gap: 8px; flex: none; color: var(--kg-text); }
|
|
155
163
|
.brand-mark { width: 20px; height: 20px; display: block; }
|
|
156
164
|
.brand-name { font-size: 14px; font-weight: 650; letter-spacing: -0.01em; }
|
|
157
|
-
.header-sep { width: 1px; height:
|
|
158
|
-
.versions { display: flex; align-items: center; gap: 8px; min-width: 0; flex:
|
|
159
|
-
mask-image: linear-gradient(90deg, #000 calc(100% - 16px), transparent); }
|
|
165
|
+
.header-sep { width: 1px; height: 18px; background: var(--kg-border-strong); flex: none; }
|
|
166
|
+
.versions { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 0 1 auto; font-size: 13px; overflow: hidden; }
|
|
160
167
|
.versions .name { font-weight: 550; color: var(--kg-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
161
168
|
.versions .arrow { color: var(--kg-text-3); flex: none; }
|
|
162
169
|
.versions .side { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
@@ -165,16 +172,9 @@ kbd {
|
|
|
165
172
|
font: 500 11px/1 var(--kg-mono); color: var(--kg-text-2); background: var(--kg-hover);
|
|
166
173
|
box-shadow: inset 0 0 0 1px var(--kg-border); white-space: nowrap; max-width: 160px; overflow: hidden; text-overflow: ellipsis;
|
|
167
174
|
}
|
|
168
|
-
.header-center { flex: none; display: flex; justify-content: center; }
|
|
169
|
-
/* header="compact": no brand or names (the host shows them); the names keep their space so the
|
|
170
|
-
mode control stays centered. */
|
|
171
|
-
:host([header="compact"]) .brand, :host([header="compact"]) .header-sep { display: none; }
|
|
172
|
-
:host([header="compact"]) .versions { visibility: hidden; }
|
|
173
|
-
.header-end { display: flex; align-items: center; gap: 4px; flex: 1 0 auto; justify-content: flex-end; }
|
|
174
|
-
.stats { display: flex; align-items: center; gap: 4px; margin-right: 6px; }
|
|
175
175
|
|
|
176
176
|
.segmented {
|
|
177
|
-
position: relative; display: inline-flex; align-items: center; gap: 2px; height: 32px; padding: 2px;
|
|
177
|
+
position: relative; display: inline-flex; align-items: center; gap: 2px; height: 32px; padding: 2px; flex: none;
|
|
178
178
|
border-radius: 9px; background: var(--kg-hover); box-shadow: inset 0 0 0 1px var(--kg-border);
|
|
179
179
|
}
|
|
180
180
|
.segmented .thumb {
|
|
@@ -191,17 +191,6 @@ kbd {
|
|
|
191
191
|
.seg[aria-checked="true"] { color: var(--kg-text); }
|
|
192
192
|
.seg .icon { opacity: 0.9; }
|
|
193
193
|
|
|
194
|
-
.pill {
|
|
195
|
-
display: inline-flex; align-items: center; gap: 4px; height: 28px; padding: 0 9px; border-radius: 14px;
|
|
196
|
-
font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap;
|
|
197
|
-
transition: box-shadow var(--kg-fast) var(--kg-ease), background var(--kg-fast) var(--kg-ease), opacity var(--kg-fast) var(--kg-ease);
|
|
198
|
-
}
|
|
199
|
-
.stat { color: var(--fam-text); background: var(--fam-soft); box-shadow: inset 0 0 0 1px transparent; }
|
|
200
|
-
.stat:hover { box-shadow: inset 0 0 0 1px var(--fam-base); }
|
|
201
|
-
.stat[aria-pressed="true"] { box-shadow: inset 0 0 0 1.5px var(--fam-base); }
|
|
202
|
-
.stats.filtering .stat[aria-pressed="false"] { opacity: 0.55; }
|
|
203
|
-
.stat .glyph { font-weight: 700; }
|
|
204
|
-
|
|
205
194
|
|
|
206
195
|
${familyRules}
|
|
207
196
|
|
|
@@ -211,7 +200,6 @@ ${familyRules}
|
|
|
211
200
|
backdrop-filter: blur(16px) saturate(140%);
|
|
212
201
|
}
|
|
213
202
|
.float { position: absolute; }
|
|
214
|
-
.top-left { top: 12px; left: 12px; }
|
|
215
203
|
.bottom-left { bottom: 16px; left: 16px; }
|
|
216
204
|
.bottom-right { bottom: 16px; right: 16px; }
|
|
217
205
|
.chip {
|
|
@@ -222,7 +210,10 @@ ${familyRules}
|
|
|
222
210
|
.map-cluster { display: flex; align-items: flex-end; gap: 8px; }
|
|
223
211
|
.zoom-bar { display: flex; flex-direction: column; padding: 2px; gap: 2px; }
|
|
224
212
|
.zoom-bar .icon-btn { width: 30px; height: 30px; }
|
|
225
|
-
.minimap-host { position: relative; width: 184px; height: 128px; border-radius: 12px; box-shadow: 0 0 0 1px var(--kg-border);
|
|
213
|
+
.minimap-host { position: relative; width: 184px; height: 128px; border-radius: 12px; box-shadow: 0 0 0 1px var(--kg-border);
|
|
214
|
+
animation: fade var(--kg-base) var(--kg-ease); }
|
|
215
|
+
/* Everything is in view: the minimap has nothing to add. */
|
|
216
|
+
.minimap-host[data-hidden="true"] { display: none; }
|
|
226
217
|
.minimap-host canvas { border-radius: 12px; }
|
|
227
218
|
|
|
228
219
|
.legend { padding: 6px; min-width: 0; }
|
|
@@ -237,30 +228,60 @@ ${familyRules}
|
|
|
237
228
|
.legend-row { display: grid; grid-template-columns: 22px auto; align-items: center; column-gap: 8px; height: 26px; padding: 0 10px 0 4px; border-radius: 6px; cursor: default; }
|
|
238
229
|
.legend-row .name { font-size: 12px; font-weight: 550; color: var(--kg-text); }
|
|
239
230
|
.swatch { width: 22px; height: 22px; display: block; }
|
|
231
|
+
.mini-legend { display: none; }
|
|
232
|
+
.stage-wrap[data-legend="mini"] .mini-legend {
|
|
233
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 2px 12px; bottom: 8px; left: 12px; max-width: calc(100% - 24px);
|
|
234
|
+
font-size: 11px; line-height: 16px; color: var(--kg-text-2);
|
|
235
|
+
}
|
|
236
|
+
.mini-item { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
|
|
237
|
+
.mini-item .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--fam-base); }
|
|
238
|
+
.mini-item[data-family="removed"] .dot { background: none; box-shadow: inset 0 0 0 1.5px var(--fam-base); }
|
|
239
|
+
.stage-wrap[data-legend="mini"] .legend, .stage-wrap[data-legend="none"] .legend { display: none; }
|
|
240
240
|
|
|
241
241
|
.caption {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
transition: opacity var(--kg-base) var(--kg-ease);
|
|
242
|
+
max-width: min(560px, 100%); display: flex; align-items: center; gap: 8px; height: 32px; padding: 0 12px 0 6px;
|
|
243
|
+
font-size: 12.5px; font-weight: 550;
|
|
245
244
|
}
|
|
246
245
|
.caption .text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
247
246
|
.caption .count { color: var(--kg-text-3); font-weight: 500; font-variant-numeric: tabular-nums; flex: none; }
|
|
248
|
-
.caption[data-empty="true"] {
|
|
247
|
+
.caption[data-empty="true"] { display: none; }
|
|
249
248
|
|
|
250
249
|
.glyph-box {
|
|
251
250
|
display: inline-flex; align-items: center; justify-content: center; flex: none; width: 20px; height: 20px; border-radius: 6px;
|
|
252
251
|
font-size: 12px; font-weight: 700; line-height: 1; color: var(--fam-text); background: var(--fam-soft);
|
|
253
252
|
}
|
|
254
253
|
|
|
255
|
-
|
|
256
|
-
.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
.
|
|
261
|
-
.
|
|
262
|
-
.
|
|
263
|
-
.
|
|
254
|
+
/* View dock (bottom center): replay's finished pill, caption and transport stack above the bar. */
|
|
255
|
+
.chrome > .dock {
|
|
256
|
+
position: absolute; left: 16px; right: 16px; bottom: 16px; display: flex; flex-direction: column; align-items: center;
|
|
257
|
+
gap: 8px; pointer-events: none;
|
|
258
|
+
}
|
|
259
|
+
.dock > * { pointer-events: auto; }
|
|
260
|
+
.dock-bar { display: flex; align-items: center; gap: 4px; height: 44px; padding: 0 6px; max-width: 100%; }
|
|
261
|
+
.dock-sep { width: 1px; height: 20px; margin: 0 2px; flex: none; background: var(--kg-border-strong); }
|
|
262
|
+
.scope-btn {
|
|
263
|
+
display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 10px; border-radius: 9px; flex: none;
|
|
264
|
+
font-size: 12.5px; font-weight: 500; color: var(--kg-text-2); white-space: nowrap;
|
|
265
|
+
transition: background var(--kg-fast) var(--kg-ease), color var(--kg-fast) var(--kg-ease), box-shadow var(--kg-fast) var(--kg-ease);
|
|
266
|
+
}
|
|
267
|
+
.scope-btn:hover { color: var(--kg-text); background: var(--kg-hover); }
|
|
268
|
+
.scope-btn[aria-pressed="true"] { color: var(--kg-accent-text); background: var(--kg-accent-soft); box-shadow: inset 0 0 0 1px var(--kg-accent-soft); }
|
|
269
|
+
.scope-btn[aria-disabled="true"] { cursor: default; }
|
|
270
|
+
.context { display: inline-flex; align-items: center; gap: 8px; padding-left: 2px; flex: none; animation: reveal var(--kg-base) var(--kg-ease); }
|
|
271
|
+
@keyframes reveal { from { opacity: 0; transform: translateX(-6px); } }
|
|
272
|
+
.context .label { font-size: 12px; font-weight: 550; color: var(--kg-text-3); white-space: nowrap; }
|
|
273
|
+
.context .opts { display: inline-flex; gap: 2px; padding: 2px; border-radius: 8px; background: var(--kg-hover); }
|
|
274
|
+
.context .opt { width: 28px; height: 28px; border-radius: 6px; font-size: 12px; font-weight: 600; color: var(--kg-text-2); font-variant-numeric: tabular-nums; }
|
|
275
|
+
.context .opt:hover { color: var(--kg-text); }
|
|
276
|
+
.context .opt[aria-checked="true"] { background: var(--kg-raised); color: var(--kg-text); box-shadow: inset 0 0 0 1px var(--kg-border-strong); }
|
|
277
|
+
.context .expand { display: inline-flex; align-items: center; gap: 4px; height: 28px; padding: 0 10px 0 8px; border-radius: 7px; font-size: 12px; font-weight: 600; color: var(--kg-text-2); box-shadow: inset 0 0 0 1px var(--kg-border-strong); font-variant-numeric: tabular-nums; }
|
|
278
|
+
.context .expand:hover:not(:disabled) { color: var(--kg-text); background: var(--kg-hover); }
|
|
279
|
+
.context .expand .icon { width: 14px; height: 14px; }
|
|
280
|
+
/* Narrower canvases keep the dock clear of the legend and the minimap: labels give way to icons
|
|
281
|
+
(each control keeps its name as a tooltip). */
|
|
282
|
+
@container (max-width: 1180px) { .context .label { display: none; } }
|
|
283
|
+
@container (max-width: 1000px) { .dock-bar .seg .lbl { display: none; } .dock-bar .seg { padding: 0 9px; } }
|
|
284
|
+
@container (max-width: 640px) { .scope-btn .lbl { display: none; } .scope-btn { padding: 0 9px; } }
|
|
264
285
|
|
|
265
286
|
/* Swipe */
|
|
266
287
|
.swipe { position: absolute; inset: 0; pointer-events: none; }
|
|
@@ -280,14 +301,14 @@ ${familyRules}
|
|
|
280
301
|
.swipe .divider:hover .handle, .swipe .divider.dragging .handle { transform: scale(1.08); }
|
|
281
302
|
.swipe .divider:focus-visible { outline: none; }
|
|
282
303
|
.swipe .divider:focus-visible .handle { box-shadow: var(--kg-shadow), 0 0 0 2px var(--kg-accent), 0 0 0 5px var(--kg-accent-soft); }
|
|
283
|
-
|
|
304
|
+
/* Below the HUD's title and actions. */
|
|
305
|
+
.swipe .side-chip { position: absolute; top: 64px; pointer-events: none; }
|
|
284
306
|
.swipe .side-chip.before { left: 12px; }
|
|
285
307
|
.swipe .side-chip.after { right: 12px; }
|
|
286
308
|
|
|
287
|
-
/* Replay transport */
|
|
309
|
+
/* Replay transport (in the dock, clear of the legend and the minimap) */
|
|
288
310
|
.transport {
|
|
289
|
-
|
|
290
|
-
min-width: 360px;
|
|
311
|
+
width: min(600px, calc(100% - 464px)); min-width: min(360px, 100%);
|
|
291
312
|
display: flex; align-items: center; gap: 4px; height: 48px; padding: 0 8px;
|
|
292
313
|
}
|
|
293
314
|
.transport .play { width: 36px; height: 36px; border-radius: 10px; color: var(--kg-text); }
|
|
@@ -310,12 +331,12 @@ ${familyRules}
|
|
|
310
331
|
display: flex; align-items: center; gap: 6px;
|
|
311
332
|
}
|
|
312
333
|
.finished {
|
|
313
|
-
|
|
334
|
+
display: flex; align-items: center; gap: 10px;
|
|
314
335
|
height: 36px; padding: 0 6px 0 12px; font-size: 12.5px; font-weight: 550; color: var(--kg-text);
|
|
315
336
|
animation: rise var(--kg-base) var(--kg-ease);
|
|
316
337
|
}
|
|
317
338
|
.finished .icon { color: var(--kg-accent-text); }
|
|
318
|
-
@keyframes rise { from { opacity: 0; transform:
|
|
339
|
+
@keyframes rise { from { opacity: 0; transform: translateY(6px); } }
|
|
319
340
|
|
|
320
341
|
/* Tooltip */
|
|
321
342
|
.tooltip {
|
|
@@ -413,7 +434,7 @@ ${familyRules}
|
|
|
413
434
|
transition: background var(--kg-fast) var(--kg-ease), box-shadow var(--kg-fast) var(--kg-ease), color var(--kg-fast) var(--kg-ease);
|
|
414
435
|
}
|
|
415
436
|
.fchip:hover { background: var(--kg-hover); color: var(--kg-text); }
|
|
416
|
-
.fchip .
|
|
437
|
+
.fchip .swatch { width: 16px; height: 16px; margin: 0 -1px; }
|
|
417
438
|
.fchip .n { color: var(--kg-text-3); font-variant-numeric: tabular-nums; font-weight: 500; }
|
|
418
439
|
.fchip[aria-pressed="true"] { background: var(--fam-soft); color: var(--fam-text); box-shadow: inset 0 0 0 1px var(--fam-base); }
|
|
419
440
|
.fchip[aria-pressed="true"] .n { color: var(--fam-text); opacity: 0.8; }
|
|
@@ -567,7 +588,7 @@ ins.hl { text-decoration: none; background: var(--kg-added-soft); color: var(--k
|
|
|
567
588
|
|
|
568
589
|
/* ── Popovers ────────────────────────────────────────────────────────────── */
|
|
569
590
|
.menu {
|
|
570
|
-
position: absolute; z-index: 10; top:
|
|
591
|
+
position: absolute; z-index: 10; top: calc(100% + 8px); right: 0; width: 240px; padding: 6px; border-radius: 12px;
|
|
571
592
|
animation: pop var(--kg-fast) var(--kg-ease);
|
|
572
593
|
}
|
|
573
594
|
@keyframes pop { from { opacity: 0; transform: translateY(-4px) scale(0.98); } }
|
|
@@ -595,6 +616,11 @@ ins.hl { text-decoration: none; background: var(--kg-added-soft); color: var(--k
|
|
|
595
616
|
.key-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 34px; border-bottom: 1px solid var(--kg-border); font-size: 12.5px; color: var(--kg-text-2); }
|
|
596
617
|
.key-row .ks { display: inline-flex; align-items: center; gap: 3px; flex: none; color: var(--kg-text-3); font-size: 11px; }
|
|
597
618
|
|
|
619
|
+
/* ── chrome="none": the graph, its hover tooltip and the legend line only ──── */
|
|
620
|
+
.root[data-chrome="none"] .hud, .root[data-chrome="none"] .sidebar, .root[data-chrome="none"] .dock,
|
|
621
|
+
.root[data-chrome="none"] .map-cluster, .root[data-chrome="none"] .side-chip { display: none !important; }
|
|
622
|
+
.stage-wrap[data-selectable="false"] .tooltip .t-hint { display: none; }
|
|
623
|
+
|
|
598
624
|
/* ── Compact (< 760 px): bottom sheet ────────────────────────────────────── */
|
|
599
625
|
.root[data-compact="true"] .body { flex-direction: column; }
|
|
600
626
|
.root[data-compact="true"] .sidebar {
|
|
@@ -610,20 +636,17 @@ ins.hl { text-decoration: none; background: var(--kg-added-soft); color: var(--k
|
|
|
610
636
|
}
|
|
611
637
|
.root[data-compact="true"] .sheet-handle::before { content: ""; width: 36px; height: 4px; border-radius: 2px; background: var(--kg-control-border); }
|
|
612
638
|
.root[data-compact="true"] .sb-head { padding-top: 4px; }
|
|
613
|
-
.root[data-compact="true"] .
|
|
614
|
-
.root[data-compact="true"] .seg { padding: 0 9px; }
|
|
615
|
-
.root[data-compact="true"] .versions, .root[data-compact="true"] .header-sep, .root[data-compact="true"] .stats,
|
|
639
|
+
.root[data-compact="true"] .versions, .root[data-compact="true"] .header-sep,
|
|
616
640
|
.root[data-compact="true"] .brand-name { display: none; }
|
|
617
|
-
.root[data-compact="true"] .
|
|
618
|
-
.root[data-compact="true"] .header-end { flex: none; }
|
|
641
|
+
.root[data-compact="true"] .hud-title { padding: 0 10px; }
|
|
619
642
|
.root[data-compact="true"] .minimap-host, .root[data-compact="true"] .legend { display: none; }
|
|
620
|
-
/* Zoom controls move
|
|
621
|
-
.root[data-compact="true"] .map-cluster { top:
|
|
622
|
-
|
|
643
|
+
/* Zoom controls move under the actions, clear of the full-width replay transport. */
|
|
644
|
+
.root[data-compact="true"] .map-cluster { top: 64px; bottom: auto; right: 12px; }
|
|
645
|
+
/* The bottom sheet's rounded top overlaps the canvas. */
|
|
646
|
+
.root[data-compact="true"] .chrome > .dock { left: 12px; right: 12px; bottom: 26px; }
|
|
647
|
+
.root[data-compact="true"] .transport { width: 100%; min-width: 0; }
|
|
623
648
|
.root[data-compact="true"] .transport .pos { min-width: 48px; }
|
|
624
649
|
.root[data-compact="true"] .detail { max-height: 50%; }
|
|
625
|
-
.root[data-compact="true"] .caption { max-width: calc(100% - 24px); }
|
|
626
|
-
.root[data-narrow="true"] .stats { display: none; }
|
|
627
650
|
.root[data-narrow="true"] .versions .tag, .root[data-narrow="true"] .versions .arrow { display: none; }
|
|
628
651
|
|
|
629
652
|
@media (prefers-reduced-motion: reduce) {
|
package/src/ui/context.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface UiContext {
|
|
|
12
12
|
readonly controller: ViewerController;
|
|
13
13
|
readonly tr: () => Translator;
|
|
14
14
|
setMode(mode: ViewerMode): void;
|
|
15
|
+
/** Toggle the `changes` scope (explains itself when it is locked). */
|
|
16
|
+
toggleScope(): void;
|
|
15
17
|
setThemePreference(pref: ThemePreference): void;
|
|
16
18
|
setPalette(palette: PaletteName): void;
|
|
17
19
|
setLocale(locale: Locale): void;
|