@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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The family filters (RFC 0001 §3, `<kgdiff-filters>` from phase 2): one chip per family the
|
|
3
|
+
* diff has, with its count and the legend's swatch, toggling the session's family filter, and
|
|
4
|
+
* "show all" while a filter is on.
|
|
5
|
+
*/
|
|
6
|
+
import { attr, h, setText } from '../dom.js';
|
|
7
|
+
import { familyCounts } from '../list-model.js';
|
|
8
|
+
import type { Topic } from '../session.js';
|
|
9
|
+
import { FAMILIES } from '../styles.js';
|
|
10
|
+
import type { UiContext } from './context.js';
|
|
11
|
+
import { swatch } from './swatch.js';
|
|
12
|
+
|
|
13
|
+
export class Filters {
|
|
14
|
+
readonly el: HTMLElement;
|
|
15
|
+
readonly #chips = new Map<string, HTMLButtonElement>();
|
|
16
|
+
readonly #showAll: HTMLButtonElement;
|
|
17
|
+
|
|
18
|
+
readonly #ctx: UiContext;
|
|
19
|
+
|
|
20
|
+
constructor(ctx: UiContext) {
|
|
21
|
+
this.#ctx = ctx;
|
|
22
|
+
const c = ctx.controller;
|
|
23
|
+
this.el = h('div', { class: 'filters', role: 'group' });
|
|
24
|
+
for (const f of FAMILIES) {
|
|
25
|
+
const chip = h(
|
|
26
|
+
'button',
|
|
27
|
+
{
|
|
28
|
+
type: 'button',
|
|
29
|
+
class: 'fchip',
|
|
30
|
+
'data-family': f,
|
|
31
|
+
'aria-pressed': 'false',
|
|
32
|
+
onclick: () => c.toggleFamily(f),
|
|
33
|
+
},
|
|
34
|
+
swatch(f),
|
|
35
|
+
h('span', { class: 'lbl' }),
|
|
36
|
+
h('span', { class: 'n' }),
|
|
37
|
+
);
|
|
38
|
+
this.#chips.set(f, chip);
|
|
39
|
+
this.el.appendChild(chip);
|
|
40
|
+
}
|
|
41
|
+
this.#showAll = h('button', {
|
|
42
|
+
type: 'button',
|
|
43
|
+
class: 'link-btn',
|
|
44
|
+
hidden: true,
|
|
45
|
+
onclick: () => c.setFamilies([]),
|
|
46
|
+
});
|
|
47
|
+
this.el.appendChild(this.#showAll);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
update(topics: ReadonlySet<Topic>): void {
|
|
51
|
+
const all = topics.has('status') || topics.has('data') || topics.has('locale');
|
|
52
|
+
if (all) this.#renderChips();
|
|
53
|
+
if (all || topics.has('filters')) this.#renderState();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#renderChips(): void {
|
|
57
|
+
const tr = this.#ctx.tr();
|
|
58
|
+
const scene = this.#ctx.controller.scene;
|
|
59
|
+
const counts = scene ? familyCounts(scene) : null;
|
|
60
|
+
attr(this.el, 'aria-label', tr.t('sidebar.filters'));
|
|
61
|
+
setText(this.#showAll, tr.t('sidebar.clearFilters'));
|
|
62
|
+
this.el.hidden = !scene || scene.hunks.length === 0;
|
|
63
|
+
for (const [f, chip] of this.#chips) {
|
|
64
|
+
const n = counts ? counts[f as keyof typeof counts] : 0;
|
|
65
|
+
setText(chip.querySelector('.lbl') as Element, tr.t(`family.${f as keyof typeof counts}`));
|
|
66
|
+
setText(chip.querySelector('.n') as Element, tr.num(n));
|
|
67
|
+
chip.hidden = n === 0;
|
|
68
|
+
attr(chip, 'title', tr.t(`familyHint.${f as keyof typeof counts}`));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#renderState(): void {
|
|
73
|
+
const fams = this.#ctx.controller.families;
|
|
74
|
+
for (const [f, chip] of this.#chips) {
|
|
75
|
+
attr(chip, 'aria-pressed', String(fams.has(f as never)));
|
|
76
|
+
}
|
|
77
|
+
this.#showAll.hidden = fams.size === 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/ui/header.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The canvas HUD: the title (brand and compared versions) floating
|
|
3
|
-
* (appearance menu, help, sidebar toggle)
|
|
4
|
-
*
|
|
2
|
+
* The canvas HUD: the title (`Title`, `<kgdiff-title>`: brand and compared versions) floating
|
|
3
|
+
* top-left, the actions (`Actions`, `<kgdiff-actions>`: appearance menu, help, sidebar toggle)
|
|
4
|
+
* floating top-right. Both stay up while loading and on errors. How the diff is viewed is
|
|
5
|
+
* chosen in the dock at the bottom (`dock.ts`).
|
|
5
6
|
*/
|
|
6
7
|
import type { Topic } from '../controller.js';
|
|
7
|
-
import { attr, h, iconButton, relabel, replace } from '../dom.js';
|
|
8
|
+
import { attr, h, iconButton, moveFocusOut, relabel, replace } from '../dom.js';
|
|
9
|
+
import type { PanelState } from '../index.js';
|
|
8
10
|
import type { UiContext } from './context.js';
|
|
9
11
|
|
|
10
12
|
/** The kgdiff mark: a soft accent diamond holding a node. */
|
|
@@ -13,21 +15,16 @@ function brandMark(): SVGSVGElement {
|
|
|
13
15
|
tpl.innerHTML =
|
|
14
16
|
'<svg class="brand-mark" viewBox="0 0 20 20" aria-hidden="true" focusable="false">' +
|
|
15
17
|
'<rect x="4.1" y="4.1" width="11.8" height="11.8" rx="3.2" transform="rotate(45 10 10)" ' +
|
|
16
|
-
'fill="var(--
|
|
17
|
-
'<circle cx="10" cy="10" r="2.4" fill="var(--
|
|
18
|
+
'fill="var(--kgdiff-accent-soft)" stroke="var(--kgdiff-accent)" stroke-width="1.5"/>' +
|
|
19
|
+
'<circle cx="10" cy="10" r="2.4" fill="var(--kgdiff-accent)"/></svg>';
|
|
18
20
|
return tpl.content.firstElementChild as SVGSVGElement;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
/** The title card: the brand, then the compared names and versions. */
|
|
24
|
+
export class Title {
|
|
22
25
|
readonly el: HTMLElement;
|
|
23
|
-
readonly menuButton: HTMLButtonElement;
|
|
24
26
|
readonly #brand: HTMLElement;
|
|
25
27
|
readonly #versions: HTMLElement;
|
|
26
|
-
readonly #actions: HTMLElement;
|
|
27
|
-
readonly #end: HTMLElement;
|
|
28
|
-
readonly #helpButton: HTMLButtonElement;
|
|
29
|
-
readonly #sidebarButton: HTMLButtonElement;
|
|
30
|
-
|
|
31
28
|
readonly #ctx: UiContext;
|
|
32
29
|
|
|
33
30
|
constructor(ctx: UiContext) {
|
|
@@ -39,60 +36,21 @@ export class Header {
|
|
|
39
36
|
h('span', { class: 'brand-name', text: 'kgdiff' }),
|
|
40
37
|
);
|
|
41
38
|
this.#versions = h('div', { class: 'versions' });
|
|
42
|
-
this.menuButton = iconButton('contrast', '', () => ctx.toggleMenu(), {
|
|
43
|
-
'aria-haspopup': 'menu',
|
|
44
|
-
'aria-expanded': 'false',
|
|
45
|
-
});
|
|
46
|
-
this.#helpButton = iconButton('help', '', () => ctx.openHelp());
|
|
47
|
-
this.#sidebarButton = iconButton('sidebar', '', () => ctx.toggleSidebar());
|
|
48
|
-
this.#actions = h(
|
|
49
|
-
'div',
|
|
50
|
-
{ class: 'hud-actions card', role: 'group' },
|
|
51
|
-
this.menuButton,
|
|
52
|
-
this.#helpButton,
|
|
53
|
-
this.#sidebarButton,
|
|
54
|
-
);
|
|
55
|
-
// The menu is a sibling of the actions card, not inside it: a backdrop filter would clip
|
|
56
|
-
// the menu's own blur to the card.
|
|
57
|
-
this.#end = h('div', { class: 'hud-end' }, this.#actions);
|
|
58
39
|
this.el = h(
|
|
59
|
-
'
|
|
60
|
-
{ class: 'hud' },
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this.#brand,
|
|
65
|
-
h('span', { class: 'header-sep', 'aria-hidden': 'true' }),
|
|
66
|
-
this.#versions,
|
|
67
|
-
),
|
|
68
|
-
this.#end,
|
|
40
|
+
'div',
|
|
41
|
+
{ class: 'hud-title card' },
|
|
42
|
+
this.#brand,
|
|
43
|
+
h('span', { class: 'header-sep', 'aria-hidden': 'true' }),
|
|
44
|
+
this.#versions,
|
|
69
45
|
);
|
|
70
46
|
}
|
|
71
47
|
|
|
72
|
-
/** Place the appearance menu under the actions. */
|
|
73
|
-
attachMenu(menu: HTMLElement): void {
|
|
74
|
-
this.#end.appendChild(menu);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
48
|
update(topics: ReadonlySet<Topic>): void {
|
|
78
49
|
if (!(topics.has('locale') || topics.has('data') || topics.has('status'))) return;
|
|
79
|
-
|
|
80
|
-
attr(this.#brand, 'title', `kgdiff — ${tr.t('brand.tagline')}`);
|
|
81
|
-
attr(this.#actions, 'aria-label', tr.t('header.actions'));
|
|
82
|
-
relabel(this.menuButton, tr.t('action.settings'));
|
|
83
|
-
relabel(this.#helpButton, `${tr.t('action.help')} (?)`);
|
|
84
|
-
relabel(this.#sidebarButton, `${tr.t('action.sidebar')} (\\)`);
|
|
50
|
+
attr(this.#brand, 'title', `kgdiff — ${this.#ctx.tr().t('brand.tagline')}`);
|
|
85
51
|
this.#renderVersions();
|
|
86
52
|
}
|
|
87
53
|
|
|
88
|
-
setSidebarState(open: boolean): void {
|
|
89
|
-
attr(this.#sidebarButton, 'aria-pressed', String(open));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
setMenuState(open: boolean): void {
|
|
93
|
-
attr(this.menuButton, 'aria-expanded', String(open));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
54
|
#renderVersions(): void {
|
|
97
55
|
const data = this.#ctx.controller.data;
|
|
98
56
|
if (!data) {
|
|
@@ -146,3 +104,116 @@ export class Header {
|
|
|
146
104
|
);
|
|
147
105
|
}
|
|
148
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The actions: the appearance menu's button (the menu itself is placed next to the card with
|
|
110
|
+
* `attachMenu`: a backdrop filter would clip its blur to the card), help, and, in the default
|
|
111
|
+
* layout, the sidebar toggle.
|
|
112
|
+
*/
|
|
113
|
+
export class Actions {
|
|
114
|
+
/** The card and, once attached, the menu next to it. */
|
|
115
|
+
readonly el: HTMLElement;
|
|
116
|
+
readonly menuButton: HTMLButtonElement;
|
|
117
|
+
readonly sidebarButton: HTMLButtonElement | null;
|
|
118
|
+
readonly #card: HTMLElement;
|
|
119
|
+
readonly #helpButton: HTMLButtonElement;
|
|
120
|
+
readonly #ctx: UiContext;
|
|
121
|
+
|
|
122
|
+
/** `sidebar`: with the sidebar toggle (the default layout's). */
|
|
123
|
+
constructor(ctx: UiContext, sidebar = true) {
|
|
124
|
+
this.#ctx = ctx;
|
|
125
|
+
this.menuButton = iconButton('contrast', '', () => ctx.toggleMenu(), {
|
|
126
|
+
'aria-haspopup': 'menu',
|
|
127
|
+
'aria-expanded': 'false',
|
|
128
|
+
});
|
|
129
|
+
this.#helpButton = iconButton('help', '', () => ctx.openHelp());
|
|
130
|
+
this.sidebarButton = sidebar ? iconButton('sidebar', '', () => ctx.toggleSidebar()) : null;
|
|
131
|
+
this.#card = h(
|
|
132
|
+
'div',
|
|
133
|
+
{ class: 'hud-actions card', role: 'group' },
|
|
134
|
+
this.menuButton,
|
|
135
|
+
this.#helpButton,
|
|
136
|
+
this.sidebarButton,
|
|
137
|
+
);
|
|
138
|
+
this.el = h('div', { class: 'hud-end' }, this.#card);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Place the appearance menu under the actions. */
|
|
142
|
+
attachMenu(menu: HTMLElement): void {
|
|
143
|
+
this.el.appendChild(menu);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
update(topics: ReadonlySet<Topic>): void {
|
|
147
|
+
if (!(topics.has('locale') || topics.has('data') || topics.has('status'))) return;
|
|
148
|
+
const tr = this.#ctx.tr();
|
|
149
|
+
attr(this.#card, 'aria-label', tr.t('header.actions'));
|
|
150
|
+
relabel(this.menuButton, tr.t('action.settings'));
|
|
151
|
+
relabel(this.#helpButton, `${tr.t('action.help')} (?)`);
|
|
152
|
+
if (this.sidebarButton) relabel(this.sidebarButton, `${tr.t('action.sidebar')} (\\)`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setMenuState(open: boolean): void {
|
|
156
|
+
attr(this.menuButton, 'aria-expanded', String(open));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Whether the sidebar is open (the toggle's pressed state). */
|
|
160
|
+
setSidebarState(open: boolean): void {
|
|
161
|
+
if (this.sidebarButton) attr(this.sidebarButton, 'aria-pressed', String(open));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The default layout's HUD: the title and the actions over the canvas. */
|
|
166
|
+
export class Header {
|
|
167
|
+
readonly el: HTMLElement;
|
|
168
|
+
readonly title: Title;
|
|
169
|
+
readonly actions: Actions;
|
|
170
|
+
readonly menuButton: HTMLButtonElement;
|
|
171
|
+
readonly sidebarButton: HTMLButtonElement;
|
|
172
|
+
readonly #ctx: UiContext;
|
|
173
|
+
|
|
174
|
+
constructor(ctx: UiContext) {
|
|
175
|
+
this.#ctx = ctx;
|
|
176
|
+
this.title = new Title(ctx);
|
|
177
|
+
this.actions = new Actions(ctx);
|
|
178
|
+
this.menuButton = this.actions.menuButton;
|
|
179
|
+
this.sidebarButton = this.actions.sidebarButton as HTMLButtonElement;
|
|
180
|
+
this.el = h('header', { class: 'hud' }, this.title.el, this.actions.el);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Place the appearance menu under the actions. */
|
|
184
|
+
attachMenu(menu: HTMLElement): void {
|
|
185
|
+
this.actions.attachMenu(menu);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
update(topics: ReadonlySet<Topic>): void {
|
|
189
|
+
this.title.update(topics);
|
|
190
|
+
this.actions.update(topics);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
setSidebarState(open: boolean): void {
|
|
194
|
+
this.actions.setSidebarState(open);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Follow the panel states of the title and the actions (not shown unless open); the sidebar
|
|
199
|
+
* button goes when the sidebar has nothing to show. Focus in the actions moves to `outside`.
|
|
200
|
+
*/
|
|
201
|
+
setPanels(
|
|
202
|
+
title: PanelState,
|
|
203
|
+
actions: PanelState,
|
|
204
|
+
sidebar: boolean,
|
|
205
|
+
outside: () => HTMLElement | null,
|
|
206
|
+
): void {
|
|
207
|
+
attr(this.title.el, 'data-panel', title);
|
|
208
|
+
const end = this.actions.el;
|
|
209
|
+
if (actions !== 'open') moveFocusOut(this.#ctx.root, end, outside);
|
|
210
|
+
attr(end, 'data-panel', actions);
|
|
211
|
+
end.inert = actions !== 'open';
|
|
212
|
+
if (!sidebar) moveFocusOut(this.#ctx.root, this.sidebarButton, outside);
|
|
213
|
+
this.sidebarButton.hidden = !sidebar;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
setMenuState(open: boolean): void {
|
|
217
|
+
this.actions.setMenuState(open);
|
|
218
|
+
}
|
|
219
|
+
}
|
package/src/ui/legend.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The legend (RFC 0001 §3, `<kgdiff-legend>`): a card of every family's swatch and name, folded
|
|
3
|
+
* to its head until opened, or one line of dots for the families this diff has, then the
|
|
4
|
+
* unchanged context. Which one shows is the layout's (`.stage-wrap[data-legend]`).
|
|
5
|
+
*/
|
|
6
|
+
import type { Family } from '@kgdiff/scene';
|
|
7
|
+
import { attr, h, icon, replace, setText } from '../dom.js';
|
|
8
|
+
import type { Translator } from '../i18n.js';
|
|
9
|
+
import { FAMILIES } from '../styles.js';
|
|
10
|
+
import type { Topic } from '../topics.js';
|
|
11
|
+
import type { UiContext } from './context.js';
|
|
12
|
+
import { swatch } from './swatch.js';
|
|
13
|
+
|
|
14
|
+
export class Legend {
|
|
15
|
+
/** The card (a toggle head and the rows). */
|
|
16
|
+
readonly card: HTMLElement;
|
|
17
|
+
/** The one-line legend. */
|
|
18
|
+
readonly mini: HTMLElement;
|
|
19
|
+
readonly #head: HTMLButtonElement;
|
|
20
|
+
readonly #body: HTMLElement;
|
|
21
|
+
#open = false;
|
|
22
|
+
|
|
23
|
+
readonly #ctx: UiContext;
|
|
24
|
+
|
|
25
|
+
/** `toggled`: the user opened or folded the card from its head. */
|
|
26
|
+
constructor(ctx: UiContext, toggled: (open: boolean) => void = () => undefined) {
|
|
27
|
+
this.#ctx = ctx;
|
|
28
|
+
this.#head = h(
|
|
29
|
+
'button',
|
|
30
|
+
{
|
|
31
|
+
type: 'button',
|
|
32
|
+
class: 'legend-head',
|
|
33
|
+
'aria-expanded': 'false',
|
|
34
|
+
onclick: () => {
|
|
35
|
+
this.setOpen(!this.#open);
|
|
36
|
+
toggled(this.#open);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
h('span', { class: 'lbl' }),
|
|
40
|
+
icon('chevron-down'),
|
|
41
|
+
);
|
|
42
|
+
this.#body = h('div', { class: 'legend-body', id: 'kg-legend' });
|
|
43
|
+
attr(this.#head, 'aria-controls', 'kg-legend');
|
|
44
|
+
this.card = h(
|
|
45
|
+
'div',
|
|
46
|
+
{ class: 'legend card float bottom-left', 'data-open': 'false' },
|
|
47
|
+
this.#head,
|
|
48
|
+
this.#body,
|
|
49
|
+
);
|
|
50
|
+
this.mini = h('div', { class: 'mini-legend float bottom-left', role: 'list' });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get open(): boolean {
|
|
54
|
+
return this.#open;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setOpen(open: boolean): void {
|
|
58
|
+
this.#open = open;
|
|
59
|
+
attr(this.card, 'data-open', String(open));
|
|
60
|
+
attr(this.#head, 'aria-expanded', String(open));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
update(topics: ReadonlySet<Topic>): void {
|
|
64
|
+
if (!(topics.has('status') || topics.has('data') || topics.has('locale'))) return;
|
|
65
|
+
const tr = this.#ctx.tr();
|
|
66
|
+
this.#renderCard(tr);
|
|
67
|
+
this.#renderMini(tr);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#renderCard(tr: Translator): void {
|
|
71
|
+
setText(this.#head.querySelector('.lbl') as Element, tr.t('legend.title'));
|
|
72
|
+
attr(this.#head, 'aria-label', tr.t('legend.toggle'));
|
|
73
|
+
const fams: Family[] = [...FAMILIES, 'neutral'];
|
|
74
|
+
replace(
|
|
75
|
+
this.#body,
|
|
76
|
+
fams.map((f) =>
|
|
77
|
+
h(
|
|
78
|
+
'div',
|
|
79
|
+
{ class: 'legend-row', 'data-family': f, title: tr.t(`familyHint.${f}`) },
|
|
80
|
+
swatch(f),
|
|
81
|
+
h('span', { class: 'name', text: tr.t(`family.${f}`) }),
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** One line: the families this diff has, then the unchanged context. */
|
|
88
|
+
#renderMini(tr: Translator): void {
|
|
89
|
+
const scene = this.#ctx.controller.scene;
|
|
90
|
+
const present = new Set<Family>(scene?.hunks.map((x) => x.family));
|
|
91
|
+
if (scene?.nodes.some((n) => n.status === 'unchanged')) present.add('neutral');
|
|
92
|
+
attr(this.mini, 'aria-label', tr.t('legend.title'));
|
|
93
|
+
replace(
|
|
94
|
+
this.mini,
|
|
95
|
+
[...FAMILIES, 'neutral' as const]
|
|
96
|
+
.filter((f) => present.has(f))
|
|
97
|
+
.map((f) =>
|
|
98
|
+
h(
|
|
99
|
+
'span',
|
|
100
|
+
{
|
|
101
|
+
class: 'mini-item',
|
|
102
|
+
role: 'listitem',
|
|
103
|
+
'data-family': f,
|
|
104
|
+
title: tr.t(`familyHint.${f}`),
|
|
105
|
+
},
|
|
106
|
+
h('span', { class: 'dot', 'aria-hidden': 'true' }),
|
|
107
|
+
tr.t(`family.${f}`),
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/ui/popovers.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { attr, h, icon, iconButton, replaceKeepingFocus } from '../dom.js';
|
|
|
7
7
|
import type { MessageKey } from '../i18n.js';
|
|
8
8
|
import type { IconName } from '../icons.js';
|
|
9
9
|
import type { ThemePreference } from '../index.js';
|
|
10
|
+
import { type Keymap, type ShortcutName, shortcutKeys } from '../keyboard.js';
|
|
10
11
|
import { FAMILIES } from '../styles.js';
|
|
11
12
|
import type { UiContext } from './context.js';
|
|
12
13
|
|
|
@@ -49,6 +50,7 @@ export class Menu {
|
|
|
49
50
|
render(): void {
|
|
50
51
|
const tr = this.#ctx.tr();
|
|
51
52
|
const c = this.#ctx.controller;
|
|
53
|
+
attr(this.el, 'aria-label', tr.t('action.settings'));
|
|
52
54
|
const t = (key: MessageKey) => () => tr.t(key);
|
|
53
55
|
const swatches = (p: PaletteName): string[] => {
|
|
54
56
|
const family = getTheme(c.themeName, p).family;
|
|
@@ -143,39 +145,79 @@ export class Menu {
|
|
|
143
145
|
|
|
144
146
|
type Label = MessageKey | readonly MessageKey[];
|
|
145
147
|
|
|
146
|
-
/**
|
|
147
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Shortcut rows, by the shortcuts they list; labels reuse the controls' own names where they
|
|
150
|
+
* exist. A row of several labels names one shortcut each.
|
|
151
|
+
*/
|
|
152
|
+
const SHORTCUTS: readonly (readonly [readonly ShortcutName[], Label])[] = [
|
|
148
153
|
[
|
|
149
|
-
[
|
|
154
|
+
['overlay', 'swipe', 'replay'],
|
|
150
155
|
['mode.overlay', 'mode.swipe', 'mode.replay'],
|
|
151
156
|
],
|
|
152
|
-
[[
|
|
153
|
-
[[
|
|
154
|
-
[[
|
|
155
|
-
[[
|
|
156
|
-
[[
|
|
157
|
-
[[
|
|
158
|
-
[[
|
|
159
|
-
[[
|
|
160
|
-
[[
|
|
157
|
+
[['scope'], 'mode.changes'],
|
|
158
|
+
[['next'], 'detail.next'],
|
|
159
|
+
[['prev'], 'detail.prev'],
|
|
160
|
+
[['first', 'last'], 'help.homeEnd'],
|
|
161
|
+
[['fly'], 'help.enter'],
|
|
162
|
+
[['escape'], 'help.esc'],
|
|
163
|
+
[['play-pause'], 'help.space'],
|
|
164
|
+
[['left', 'right'], 'help.arrows'],
|
|
165
|
+
[['fit'], 'action.fit'],
|
|
161
166
|
[
|
|
162
|
-
[
|
|
167
|
+
['zoom-in', 'zoom-out'],
|
|
163
168
|
['action.zoomIn', 'action.zoomOut'],
|
|
164
169
|
],
|
|
165
|
-
[[
|
|
166
|
-
[[
|
|
167
|
-
[[
|
|
168
|
-
[[
|
|
169
|
-
[[
|
|
170
|
-
[[
|
|
170
|
+
[['search'], 'sidebar.search'],
|
|
171
|
+
[['review'], 'review.mark'],
|
|
172
|
+
[['sidebar'], 'action.sidebar'],
|
|
173
|
+
[['theme'], 'help.theme'],
|
|
174
|
+
[['expand'], 'help.expand'],
|
|
175
|
+
[['help'], 'action.help'],
|
|
171
176
|
];
|
|
177
|
+
|
|
178
|
+
/** How a key shows in the dialog. */
|
|
179
|
+
const KEY_LABELS: Readonly<Record<string, string>> = {
|
|
180
|
+
ArrowDown: '↓',
|
|
181
|
+
ArrowUp: '↑',
|
|
182
|
+
ArrowLeft: '←',
|
|
183
|
+
ArrowRight: '→',
|
|
184
|
+
' ': 'Space',
|
|
185
|
+
Escape: 'Esc',
|
|
186
|
+
'-': '−',
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/** The dialog's rows for the keys bound now: a shortcut with no key is left out. */
|
|
190
|
+
function shortcutRows(
|
|
191
|
+
keymap: Keymap | null,
|
|
192
|
+
omit: ReadonlySet<ShortcutName>,
|
|
193
|
+
): { keys: string[]; label: Label }[] {
|
|
194
|
+
const bound = shortcutKeys(keymap);
|
|
195
|
+
const keysOf = (name: ShortcutName): string[] =>
|
|
196
|
+
omit.has(name) ? [] : (bound.get(name) ?? []).map((k) => KEY_LABELS[k] ?? k);
|
|
197
|
+
const rows: { keys: string[]; label: Label }[] = [];
|
|
198
|
+
for (const [names, label] of SHORTCUTS) {
|
|
199
|
+
const keys = names.map(keysOf);
|
|
200
|
+
if (typeof label !== 'string' && keys.some((k) => k.length !== 1)) {
|
|
201
|
+
// Not one key each: a row per shortcut.
|
|
202
|
+
names.forEach((_, i) => {
|
|
203
|
+
const k = keys[i] ?? [];
|
|
204
|
+
if (k.length > 0) rows.push({ keys: k, label: label[i] as MessageKey });
|
|
205
|
+
});
|
|
206
|
+
} else if (keys.flat().length > 0) rows.push({ keys: keys.flat(), label });
|
|
207
|
+
}
|
|
208
|
+
return rows;
|
|
209
|
+
}
|
|
210
|
+
|
|
172
211
|
export class HelpDialog {
|
|
173
212
|
readonly el: HTMLElement;
|
|
174
213
|
#returnFocus: Element | null = null;
|
|
175
214
|
readonly #ctx: UiContext;
|
|
215
|
+
readonly #omit: ReadonlySet<ShortcutName>;
|
|
176
216
|
|
|
177
|
-
|
|
217
|
+
/** `omit`: shortcuts that do nothing in this layout (`sidebar` without a sidebar). */
|
|
218
|
+
constructor(ctx: UiContext, omit: readonly ShortcutName[] = []) {
|
|
178
219
|
this.#ctx = ctx;
|
|
220
|
+
this.#omit = new Set(omit);
|
|
179
221
|
this.el = h('div', { class: 'overlay-scrim', hidden: true });
|
|
180
222
|
this.el.addEventListener('click', (e) => {
|
|
181
223
|
if (e.target === this.el) this.hide();
|
|
@@ -212,18 +254,19 @@ export class HelpDialog {
|
|
|
212
254
|
|
|
213
255
|
render(): void {
|
|
214
256
|
const tr = this.#ctx.tr();
|
|
215
|
-
const keys = (
|
|
257
|
+
const keys = (list: readonly string[]): HTMLElement =>
|
|
216
258
|
h(
|
|
217
259
|
'span',
|
|
218
260
|
{ class: 'ks' },
|
|
219
|
-
|
|
261
|
+
list.map((k, i) => [
|
|
220
262
|
i > 0 ? h('span', { 'aria-hidden': 'true', text: '/' }) : null,
|
|
221
|
-
|
|
263
|
+
h('kbd', { text: k }),
|
|
222
264
|
]),
|
|
223
265
|
);
|
|
224
|
-
|
|
266
|
+
// The keys bound now, after the root's `keymap`.
|
|
267
|
+
const rows = shortcutRows(this.#ctx.keymap(), this.#omit).map(({ keys: list, label }) => {
|
|
225
268
|
const text = typeof label === 'string' ? tr.t(label) : label.map((k) => tr.t(k)).join(' / ');
|
|
226
|
-
return h('div', { class: 'key-row' }, h('span', { text }), keys(
|
|
269
|
+
return h('div', { class: 'key-row' }, h('span', { text }), keys(list));
|
|
227
270
|
});
|
|
228
271
|
replaceKeepingFocus(
|
|
229
272
|
this.#ctx.root,
|