@kgdiff/viewer 0.5.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.
Files changed (232) hide show
  1. package/README.md +43 -16
  2. package/custom-elements.json +2347 -0
  3. package/dist/bind.d.ts +41 -0
  4. package/dist/bind.d.ts.map +1 -0
  5. package/dist/bind.js +47 -0
  6. package/dist/bind.js.map +1 -0
  7. package/dist/controller.d.ts +22 -74
  8. package/dist/controller.d.ts.map +1 -1
  9. package/dist/controller.js +35 -376
  10. package/dist/controller.js.map +1 -1
  11. package/dist/dom.d.ts +10 -0
  12. package/dist/dom.d.ts.map +1 -1
  13. package/dist/dom.js +29 -0
  14. package/dist/dom.js.map +1 -1
  15. package/dist/element.d.ts +52 -46
  16. package/dist/element.d.ts.map +1 -1
  17. package/dist/element.js +362 -403
  18. package/dist/element.js.map +1 -1
  19. package/dist/elements/actions.d.ts +27 -0
  20. package/dist/elements/actions.d.ts.map +1 -0
  21. package/dist/elements/actions.js +81 -0
  22. package/dist/elements/actions.js.map +1 -0
  23. package/dist/elements/bound.d.ts +63 -0
  24. package/dist/elements/bound.d.ts.map +1 -0
  25. package/dist/elements/bound.js +213 -0
  26. package/dist/elements/bound.js.map +1 -0
  27. package/dist/elements/change-detail.d.ts +25 -0
  28. package/dist/elements/change-detail.d.ts.map +1 -0
  29. package/dist/elements/change-detail.js +19 -0
  30. package/dist/elements/change-detail.js.map +1 -0
  31. package/dist/elements/change-list.d.ts +27 -0
  32. package/dist/elements/change-list.d.ts.map +1 -0
  33. package/dist/elements/change-list.js +29 -0
  34. package/dist/elements/change-list.js.map +1 -0
  35. package/dist/elements/filters.d.ts +25 -0
  36. package/dist/elements/filters.d.ts.map +1 -0
  37. package/dist/elements/filters.js +19 -0
  38. package/dist/elements/filters.js.map +1 -0
  39. package/dist/elements/legend.d.ts +40 -0
  40. package/dist/elements/legend.d.ts.map +1 -0
  41. package/dist/elements/legend.js +87 -0
  42. package/dist/elements/legend.js.map +1 -0
  43. package/dist/elements/minimap.d.ts +38 -0
  44. package/dist/elements/minimap.d.ts.map +1 -0
  45. package/dist/elements/minimap.js +59 -0
  46. package/dist/elements/minimap.js.map +1 -0
  47. package/dist/elements/root.d.ts +40 -0
  48. package/dist/elements/root.d.ts.map +1 -0
  49. package/dist/elements/root.js +67 -0
  50. package/dist/elements/root.js.map +1 -0
  51. package/dist/elements/search.d.ts +29 -0
  52. package/dist/elements/search.d.ts.map +1 -0
  53. package/dist/elements/search.js +23 -0
  54. package/dist/elements/search.js.map +1 -0
  55. package/dist/elements/stage-context.d.ts +31 -0
  56. package/dist/elements/stage-context.d.ts.map +1 -0
  57. package/dist/elements/stage-context.js +4 -0
  58. package/dist/elements/stage-context.js.map +1 -0
  59. package/dist/elements/stage.d.ts +65 -0
  60. package/dist/elements/stage.d.ts.map +1 -0
  61. package/dist/elements/stage.js +259 -0
  62. package/dist/elements/stage.js.map +1 -0
  63. package/dist/elements/title.d.ts +25 -0
  64. package/dist/elements/title.d.ts.map +1 -0
  65. package/dist/elements/title.js +19 -0
  66. package/dist/elements/title.js.map +1 -0
  67. package/dist/elements/transport.d.ts +29 -0
  68. package/dist/elements/transport.d.ts.map +1 -0
  69. package/dist/elements/transport.js +24 -0
  70. package/dist/elements/transport.js.map +1 -0
  71. package/dist/elements/view-bound.d.ts +39 -0
  72. package/dist/elements/view-bound.d.ts.map +1 -0
  73. package/dist/elements/view-bound.js +88 -0
  74. package/dist/elements/view-bound.js.map +1 -0
  75. package/dist/elements/view-dock.d.ts +28 -0
  76. package/dist/elements/view-dock.d.ts.map +1 -0
  77. package/dist/elements/view-dock.js +28 -0
  78. package/dist/elements/view-dock.js.map +1 -0
  79. package/dist/elements/zoom.d.ts +28 -0
  80. package/dist/elements/zoom.d.ts.map +1 -0
  81. package/dist/elements/zoom.js +22 -0
  82. package/dist/elements/zoom.js.map +1 -0
  83. package/dist/i18n.d.ts +10 -0
  84. package/dist/i18n.d.ts.map +1 -1
  85. package/dist/i18n.js +20 -0
  86. package/dist/i18n.js.map +1 -1
  87. package/dist/icons.d.ts +1 -0
  88. package/dist/icons.d.ts.map +1 -1
  89. package/dist/icons.js +1 -0
  90. package/dist/icons.js.map +1 -1
  91. package/dist/index.d.ts +328 -34
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +13 -0
  94. package/dist/index.js.map +1 -1
  95. package/dist/keyboard.d.ts +39 -2
  96. package/dist/keyboard.d.ts.map +1 -1
  97. package/dist/keyboard.js +145 -62
  98. package/dist/keyboard.js.map +1 -1
  99. package/dist/layout-store.d.ts +26 -0
  100. package/dist/layout-store.d.ts.map +1 -0
  101. package/dist/layout-store.js +46 -0
  102. package/dist/layout-store.js.map +1 -0
  103. package/dist/panel-levels.d.ts +18 -0
  104. package/dist/panel-levels.d.ts.map +1 -0
  105. package/dist/panel-levels.js +41 -0
  106. package/dist/panel-levels.js.map +1 -0
  107. package/dist/panels.d.ts +28 -0
  108. package/dist/panels.d.ts.map +1 -0
  109. package/dist/panels.js +73 -0
  110. package/dist/panels.js.map +1 -0
  111. package/dist/prefs.d.ts +5 -2
  112. package/dist/prefs.d.ts.map +1 -1
  113. package/dist/prefs.js +12 -3
  114. package/dist/prefs.js.map +1 -1
  115. package/dist/react.d.ts +460 -0
  116. package/dist/react.d.ts.map +1 -0
  117. package/dist/react.js +2 -0
  118. package/dist/react.js.map +1 -0
  119. package/dist/root-element.d.ts +122 -0
  120. package/dist/root-element.d.ts.map +1 -0
  121. package/dist/root-element.js +541 -0
  122. package/dist/root-element.js.map +1 -0
  123. package/dist/root.d.ts +30 -30
  124. package/dist/root.d.ts.map +1 -1
  125. package/dist/root.js +65 -42
  126. package/dist/root.js.map +1 -1
  127. package/dist/session.d.ts +19 -3
  128. package/dist/session.d.ts.map +1 -1
  129. package/dist/session.js +15 -17
  130. package/dist/session.js.map +1 -1
  131. package/dist/standalone/kgdiff-viewer.js +4 -4
  132. package/dist/styles.d.ts +4 -0
  133. package/dist/styles.d.ts.map +1 -1
  134. package/dist/styles.js +224 -84
  135. package/dist/styles.js.map +1 -1
  136. package/dist/theme.d.ts +7 -0
  137. package/dist/theme.d.ts.map +1 -1
  138. package/dist/theme.js +10 -7
  139. package/dist/theme.js.map +1 -1
  140. package/dist/topics.d.ts +8 -0
  141. package/dist/topics.d.ts.map +1 -0
  142. package/dist/topics.js +18 -0
  143. package/dist/topics.js.map +1 -0
  144. package/dist/ui/change-list.d.ts.map +1 -1
  145. package/dist/ui/change-list.js +2 -2
  146. package/dist/ui/change-list.js.map +1 -1
  147. package/dist/ui/context.d.ts +20 -7
  148. package/dist/ui/context.d.ts.map +1 -1
  149. package/dist/ui/context.js.map +1 -1
  150. package/dist/ui/header.d.ts +41 -3
  151. package/dist/ui/header.d.ts.map +1 -1
  152. package/dist/ui/header.js +99 -34
  153. package/dist/ui/header.js.map +1 -1
  154. package/dist/ui/legend.d.ts +15 -0
  155. package/dist/ui/legend.d.ts.map +1 -0
  156. package/dist/ui/legend.js +68 -0
  157. package/dist/ui/legend.js.map +1 -0
  158. package/dist/ui/popovers.d.ts +3 -1
  159. package/dist/ui/popovers.d.ts.map +1 -1
  160. package/dist/ui/popovers.js +62 -23
  161. package/dist/ui/popovers.js.map +1 -1
  162. package/dist/ui/search.d.ts.map +1 -1
  163. package/dist/ui/search.js +3 -2
  164. package/dist/ui/search.js.map +1 -1
  165. package/dist/ui/sidebar.d.ts +26 -3
  166. package/dist/ui/sidebar.d.ts.map +1 -1
  167. package/dist/ui/sidebar.js +344 -12
  168. package/dist/ui/sidebar.js.map +1 -1
  169. package/dist/ui/stage.d.ts +56 -1
  170. package/dist/ui/stage.d.ts.map +1 -1
  171. package/dist/ui/stage.js +244 -256
  172. package/dist/ui/stage.js.map +1 -1
  173. package/dist/ui/transport.d.ts +12 -0
  174. package/dist/ui/transport.d.ts.map +1 -0
  175. package/dist/ui/transport.js +198 -0
  176. package/dist/ui/transport.js.map +1 -0
  177. package/dist/ui/zoom.d.ts +15 -0
  178. package/dist/ui/zoom.d.ts.map +1 -0
  179. package/dist/ui/zoom.js +30 -0
  180. package/dist/ui/zoom.js.map +1 -0
  181. package/dist/view.d.ts +94 -0
  182. package/dist/view.d.ts.map +1 -0
  183. package/dist/view.js +441 -0
  184. package/dist/view.js.map +1 -0
  185. package/package.json +95 -12
  186. package/src/bind.ts +80 -0
  187. package/src/controller.ts +51 -442
  188. package/src/dom.ts +30 -0
  189. package/src/element.ts +364 -406
  190. package/src/elements/actions.ts +94 -0
  191. package/src/elements/bound.ts +247 -0
  192. package/src/elements/change-detail.ts +34 -0
  193. package/src/elements/change-list.ts +51 -0
  194. package/src/elements/filters.ts +34 -0
  195. package/src/elements/legend.ts +103 -0
  196. package/src/elements/minimap.ts +75 -0
  197. package/src/elements/root.ts +86 -0
  198. package/src/elements/search.ts +38 -0
  199. package/src/elements/stage-context.ts +34 -0
  200. package/src/elements/stage.ts +285 -0
  201. package/src/elements/title.ts +34 -0
  202. package/src/elements/transport.ts +39 -0
  203. package/src/elements/view-bound.ts +118 -0
  204. package/src/elements/view-dock.ts +43 -0
  205. package/src/elements/zoom.ts +38 -0
  206. package/src/i18n.ts +20 -0
  207. package/src/icons.ts +1 -0
  208. package/src/index.ts +347 -34
  209. package/src/keyboard.ts +188 -61
  210. package/src/layout-store.ts +71 -0
  211. package/src/panel-levels.ts +53 -0
  212. package/src/panels.ts +81 -0
  213. package/src/prefs.ts +12 -4
  214. package/src/react.ts +459 -0
  215. package/src/root-element.ts +624 -0
  216. package/src/root.ts +92 -65
  217. package/src/session.ts +22 -33
  218. package/src/styles.ts +245 -84
  219. package/src/theme.ts +10 -7
  220. package/src/topics.ts +37 -0
  221. package/src/ui/change-list.ts +6 -2
  222. package/src/ui/context.ts +20 -7
  223. package/src/ui/header.ts +127 -56
  224. package/src/ui/legend.ts +112 -0
  225. package/src/ui/popovers.ts +68 -25
  226. package/src/ui/search.ts +10 -5
  227. package/src/ui/sidebar.ts +386 -23
  228. package/src/ui/stage.ts +267 -331
  229. package/src/ui/transport.ts +240 -0
  230. package/src/ui/zoom.ts +45 -0
  231. package/src/view.ts +516 -0
  232. package/vscode.html-custom-data.json +726 -0
@@ -0,0 +1,86 @@
1
+ /**
2
+ * `<kgdiff-root>` (RFC 0001 §3), documented on its class. Importing this module defines it.
3
+ */
4
+ import type { KgdiffRootElement } from '../index.js';
5
+ import { ROOT_ATTRIBUTES, RootElement } from '../root-element.js';
6
+ import { DiffSession, type SessionDeps, type SessionHost } from '../session.js';
7
+ import { elementSheet } from '../styles.js';
8
+ import type { UiActions } from '../ui/context.js';
9
+ import { define } from './bound.js';
10
+
11
+ /**
12
+ * A root without visuals. It owns the session and provides it to the elements inside it
13
+ * (`<kgdiff-stage>`, `<kgdiff-change-list>`, …) or naming it with `for`, and owns what they share:
14
+ * the keyboard map, the live region announcing navigation, the events and the theme tokens. It has
15
+ * the attributes, properties, methods and events of `<kgdiff-viewer>` except the layout's
16
+ * (`panels`, `sidebar`, `header`, `chrome`, `legend`, `persist`): the page lays the elements out,
17
+ * and nothing is remembered.
18
+ *
19
+ * It renders as `display: contents`, so its children are laid out by the page as if it were not
20
+ * there.
21
+ *
22
+ * @tagname kgdiff-root
23
+ * @experimental In 0.6; stable from 0.7 (RFC 0001, decision 7).
24
+ */
25
+ export class KgdiffRoot extends RootElement implements KgdiffRootElement {
26
+ static get observedAttributes(): readonly string[] {
27
+ return ROOT_ATTRIBUTES;
28
+ }
29
+
30
+ #built = false;
31
+
32
+ constructor() {
33
+ super();
34
+ this.root.attach({
35
+ update: (topics) => {
36
+ this.reflectSession(topics);
37
+ if (topics.has('locale') || topics.has('status')) {
38
+ this.root.live.setAttribute('lang', this.controller.locale);
39
+ }
40
+ },
41
+ });
42
+ }
43
+
44
+ protected sheets(doc: Document): readonly CSSStyleSheet[] {
45
+ return [elementSheet(doc, 'root')];
46
+ }
47
+
48
+ /** A session without a canvas of its own: each `<kgdiff-stage>` brings its view. */
49
+ protected createSession(host: SessionHost, deps: SessionDeps): DiffSession {
50
+ return new DiffSession(host, deps);
51
+ }
52
+
53
+ /** No layout of its own: `\` does nothing, `?` and `/` go to the elements registered last. */
54
+ protected actions(): UiActions {
55
+ return {
56
+ ...this.baseActions(),
57
+ toggleSidebar: () => undefined,
58
+ sidebarOpen: () => false,
59
+ toggleMenu: () => undefined,
60
+ // `?` opens the dialog of a `<kgdiff-actions>` bound to this root, if there is one.
61
+ openHelp: () => {
62
+ this.root.focus('help');
63
+ },
64
+ focusSearch: () => {
65
+ this.root.focus('search');
66
+ },
67
+ };
68
+ }
69
+
70
+ protected connected(): void {
71
+ if (this.#built) return;
72
+ this.#built = true;
73
+ this.shadow.append(document.createElement('slot'), this.root.live);
74
+ }
75
+
76
+ protected disconnected(): void {}
77
+ }
78
+
79
+ define('kgdiff-root', KgdiffRoot);
80
+
81
+ declare global {
82
+ interface HTMLElementTagNameMap {
83
+ /** @experimental In 0.6. */
84
+ 'kgdiff-root': KgdiffRootElement;
85
+ }
86
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `<kgdiff-search>` (RFC 0001 §3), documented on its class. Importing this module defines it.
3
+ */
4
+ import type { Region } from '../bind.js';
5
+ import type { KgdiffBoundElement } from '../index.js';
6
+ import type { UiContext } from '../ui/context.js';
7
+ import { Search } from '../ui/search.js';
8
+ import { BoundElement, define } from './bound.js';
9
+
10
+ /**
11
+ * Node search, a combobox and the results list it owns (its `aria-controls` and
12
+ * `aria-activedescendant` stay inside the element). The results open below the box while the query
13
+ * is not blank; ↑ / ↓ move through them, Enter chooses one (its node and change are selected, and
14
+ * focus moves to the change list), Esc clears. The query lives in the session, so a change list
15
+ * bound to the same root steps aside meanwhile. Registers itself as the root's `search` focus
16
+ * target: `/` focuses the one registered last that is shown.
17
+ *
18
+ * @tagname kgdiff-search
19
+ * @experimental In 0.6; stable from 0.7 (RFC 0001, decision 7).
20
+ */
21
+ export class KgdiffSearch extends BoundElement implements KgdiffBoundElement {
22
+ protected readonly name = 'search';
23
+
24
+ protected build(ui: UiContext): Region {
25
+ const search = new Search(ui);
26
+ this.shadow.append(search.box, search.results);
27
+ return search;
28
+ }
29
+ }
30
+
31
+ define('kgdiff-search', KgdiffSearch);
32
+
33
+ declare global {
34
+ interface HTMLElementTagNameMap {
35
+ /** @experimental In 0.6. */
36
+ 'kgdiff-search': KgdiffBoundElement;
37
+ }
38
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The view-level context a `<kgdiff-stage>` provides to its descendants (`kgdiff.stage/1`), in a
3
+ * module of its own: the chrome that acts on a view finds its stage through it without pulling
4
+ * the stage (and the renderer) into its subpath.
5
+ */
6
+ import type { ViewerContext } from '../bind.js';
7
+ import { createContext } from '../context.js';
8
+ import type { KgdiffStageElement, KgdiffViewerElement } from '../index.js';
9
+ import type { StageView } from '../view.js';
10
+
11
+ /**
12
+ * What a stage provides to its descendants (the view-level context).
13
+ *
14
+ * @experimental Like the stage, in 0.6.
15
+ */
16
+ export interface StageContext {
17
+ /** Keep `element` clear of fits and flights; returns the function that stops it. */
18
+ registerObstacle(element: Element): () => void;
19
+ }
20
+
21
+ /** @internal What the chrome that acts on a view (`<kgdiff-zoom>`, `<kgdiff-minimap>`) gets. */
22
+ export interface StageViewContext extends StageContext {
23
+ /** The stage element, or the viewer (its built-in stage). */
24
+ readonly stage: KgdiffStageElement | KgdiffViewerElement;
25
+ /** The root the stage is bound to now, or null while it waits for one. */
26
+ readonly root: ViewerContext | null;
27
+ /** The stage's view now, or null while the stage waits for its root. */
28
+ readonly view: StageView | null;
29
+ /** Call `listener` with the view now and whenever it changes; returns the stop. */
30
+ watchView(listener: (view: StageView | null) => void): () => void;
31
+ }
32
+
33
+ /** @experimental The stage context's key (`Symbol.for('kgdiff.stage/1')`). */
34
+ export const stageContext = createContext<StageContext>('kgdiff.stage/1');
@@ -0,0 +1,285 @@
1
+ /**
2
+ * `<kgdiff-stage>` (RFC 0001 §3), documented on its class. Importing this module defines it.
3
+ */
4
+ import type { Region, ViewerContext } from '../bind.js';
5
+ import { provideContext } from '../context.js';
6
+ import type {
7
+ KgdiffStageElement,
8
+ PanelState,
9
+ StagePanels,
10
+ StageRegion,
11
+ StageVersion,
12
+ } from '../index.js';
13
+ import { DEFAULT_PANELS, formatPanels, toPanels } from '../panels.js';
14
+ import type { UiContext } from '../ui/context.js';
15
+ import { Stage } from '../ui/stage.js';
16
+ import { StageView } from '../view.js';
17
+ import { BoundElement, define } from './bound.js';
18
+ import { type StageViewContext, stageContext } from './stage-context.js';
19
+
20
+ export type { StageContext } from './stage-context.js';
21
+ export { stageContext } from './stage-context.js';
22
+
23
+ /**
24
+ * The regions of the built-in chrome a stage has.
25
+ *
26
+ * @experimental Like the stage, in 0.6.
27
+ */
28
+ export const STAGE_REGIONS: readonly StageRegion[] = [
29
+ 'dock',
30
+ 'transport',
31
+ 'legend',
32
+ 'zoom',
33
+ 'minimap',
34
+ ];
35
+
36
+ type MutableStagePanels = { -readonly [R in StageRegion]?: PanelState };
37
+
38
+ const VERSIONS: readonly StageVersion[] = ['before', 'after'];
39
+
40
+ /** A version from an attribute or a property; anything else follows the mode, with a warning. */
41
+ function toVersion(value: unknown): StageVersion | null {
42
+ if (value === null || value === undefined || value === '') return null;
43
+ if (VERSIONS.includes(value as StageVersion)) return value as StageVersion;
44
+ console.warn(`[kgdiff] <kgdiff-stage> ignoring version ${JSON.stringify(value)} (before, after)`);
45
+ return null;
46
+ }
47
+
48
+ /** Panels from an attribute or a property, keeping the stage's regions (warning about the rest). */
49
+ function stagePanels(value: unknown): StagePanels {
50
+ const { panels, invalid } = toPanels(value);
51
+ const out: MutableStagePanels = {};
52
+ for (const [region, state] of Object.entries(panels)) {
53
+ if (STAGE_REGIONS.includes(region as StageRegion)) out[region as StageRegion] = state;
54
+ else invalid.push(`${region}:${state}`);
55
+ }
56
+ if (invalid.length > 0) {
57
+ console.warn(`[kgdiff] <kgdiff-stage> ignoring invalid panels: ${invalid.join(' ')}`);
58
+ }
59
+ return out;
60
+ }
61
+
62
+ /**
63
+ * The canvas of the root it is bound to: the graph, the hover tooltip, the swipe divider, the
64
+ * replay caption and the canvas chrome (view dock, replay transport, legend, zoom buttons and
65
+ * minimap). Each stage is a view of its own: a root draws every stage bound to it, each with its
66
+ * own camera, sharing the selection, the hover, the filters and the search; `version` pins one
67
+ * to a version.
68
+ *
69
+ * Five named slots take the page's content over the canvas. The stage positions it (12–16 px
70
+ * from the edges, stacked further from the corner than the chrome there), and every slotted
71
+ * element is an obstacle: fits and flights frame the graph in the free part of the canvas. A
72
+ * chrome element slotted in replaces the built-in region of its kind; `panels` keeps applying
73
+ * to the rest. Content positioned some other way registers itself with `registerObstacle`, on
74
+ * the element or on the stage context (`kgdiff.stage/1`) it provides to its descendants.
75
+ *
76
+ * @tagname kgdiff-stage
77
+ * @slot top-start - The page's content in the top left corner, below the version chip.
78
+ * @slot top-end - The page's content in the top right corner.
79
+ * @slot bottom-start - The page's content in the bottom left corner, above the legend.
80
+ * @slot bottom-center - The page's content above the view dock and the transport.
81
+ * @slot bottom-end - The page's content in the bottom right corner, above the zoom buttons and the minimap.
82
+ * @experimental In 0.6; stable from 0.7 (RFC 0001, decision 7).
83
+ */
84
+ export class KgdiffStage extends BoundElement implements KgdiffStageElement {
85
+ static override get observedAttributes(): readonly string[] {
86
+ return ['for', 'panels', 'version'];
87
+ }
88
+
89
+ protected readonly name = 'stage';
90
+ #stage: Stage | null = null;
91
+ /** The page's `panels`, and what the user changed since (the legend's head). */
92
+ #declared: StagePanels = {};
93
+ readonly #runtime: MutableStagePanels = {};
94
+ #reflecting = false;
95
+ #version: StageVersion | null = null;
96
+ #view: StageView | null = null;
97
+ readonly #viewListeners = new Set<(view: StageView | null) => void>();
98
+ /** Registered obstacles, kept across rebinding (the stage is rebuilt then). */
99
+ readonly #obstacles = new Map<Element, () => void>();
100
+
101
+ get panels(): StagePanels {
102
+ return { ...this.#declared };
103
+ }
104
+
105
+ set panels(value: StagePanels | string | null) {
106
+ const panels = stagePanels(value);
107
+ this.#reflecting = true;
108
+ try {
109
+ const text = formatPanels(panels);
110
+ if (text) this.setAttribute('panels', text);
111
+ else this.removeAttribute('panels');
112
+ } finally {
113
+ this.#reflecting = false;
114
+ }
115
+ this.#declare(panels);
116
+ }
117
+
118
+ /**
119
+ * The version this stage draws whatever the mode, reflected to the `version` attribute:
120
+ * `before` or `after` (side by side with another stage of the same root), or null to follow
121
+ * the root's mode (overlay, swipe, replay).
122
+ */
123
+ get version(): StageVersion | null {
124
+ return this.#version;
125
+ }
126
+
127
+ set version(value: StageVersion | null) {
128
+ const v = toVersion(value);
129
+ this.#reflecting = true;
130
+ try {
131
+ if (v) this.setAttribute('version', v);
132
+ else this.removeAttribute('version');
133
+ } finally {
134
+ this.#reflecting = false;
135
+ }
136
+ this.#setVersion(v);
137
+ }
138
+
139
+ override attributeChangedCallback(name: string, old: string | null, value: string | null): void {
140
+ if (this.#reflecting) return;
141
+ if (name === 'panels') this.#declare(stagePanels(value ?? ''));
142
+ else if (name === 'version') this.#setVersion(toVersion(value));
143
+ else super.attributeChangedCallback(name, old, value);
144
+ }
145
+
146
+ protected override upgradedProperties(): readonly string[] {
147
+ return [...super.upgradedProperties(), 'panels', 'version'];
148
+ }
149
+
150
+ /** @internal This stage's view (its camera), for the dev harness and tests. */
151
+ get view(): StageView | null {
152
+ return this.#view;
153
+ }
154
+
155
+ #setVersion(version: StageVersion | null): void {
156
+ if (version === this.#version) return;
157
+ this.#version = version;
158
+ if (this.#view) this.#view.version = version;
159
+ this.#stage?.setVersion(version);
160
+ }
161
+
162
+ #setView(view: StageView | null): void {
163
+ this.#view = view;
164
+ for (const listener of this.#viewListeners) listener(view);
165
+ }
166
+
167
+ /** A region the page declares another state for gives way; the same again changes nothing. */
168
+ #declare(panels: StagePanels): void {
169
+ for (const r of STAGE_REGIONS) if (panels[r] !== this.#declared[r]) delete this.#runtime[r];
170
+ this.#declared = panels;
171
+ this.#applyPanels();
172
+ }
173
+
174
+ #applyPanels(): void {
175
+ const states = { ...DEFAULT_PANELS, ...this.#declared, ...this.#runtime };
176
+ this.#stage?.setPanels(states);
177
+ }
178
+
179
+ registerObstacle(element: Element): () => void {
180
+ if (!this.#obstacles.has(element)) {
181
+ this.#obstacles.set(element, this.#stage?.registerObstacle(element) ?? (() => undefined));
182
+ }
183
+ return () => {
184
+ this.#obstacles.get(element)?.();
185
+ this.#obstacles.delete(element);
186
+ };
187
+ }
188
+
189
+ protected build(ui: UiContext, ctx: ViewerContext, release: (fn: () => void) => void): Region {
190
+ release(
191
+ provideContext(this, stageContext, (): StageViewContext => {
192
+ const stage = this;
193
+ return {
194
+ stage,
195
+ get root() {
196
+ return stage.context;
197
+ },
198
+ registerObstacle: (el) => stage.registerObstacle(el),
199
+ get view() {
200
+ return stage.#view;
201
+ },
202
+ watchView: (listener) => {
203
+ stage.#viewListeners.add(listener);
204
+ listener(stage.#view);
205
+ return () => {
206
+ stage.#viewListeners.delete(listener);
207
+ };
208
+ },
209
+ };
210
+ }),
211
+ );
212
+ // The user opening or folding the legend holds until the page declares the legend again.
213
+ const stage = new Stage(
214
+ {
215
+ ...ui,
216
+ setPanel: (region, state) => {
217
+ if (STAGE_REGIONS.includes(region as StageRegion)) {
218
+ this.#runtime[region as StageRegion] = state;
219
+ }
220
+ },
221
+ },
222
+ true,
223
+ );
224
+ this.#stage = stage;
225
+ this.#applyPanels();
226
+ for (const el of this.#obstacles.keys()) this.#obstacles.set(el, stage.registerObstacle(el));
227
+ release(() => {
228
+ this.#stage = null;
229
+ });
230
+ stage.setChrome('full', true);
231
+ // One of the page's regions, not its main landmark.
232
+ stage.asRegion();
233
+ this.shadow.append(stage.el);
234
+ // Content slotted before the stage was built.
235
+ stage.slotted();
236
+ // This stage's own view of the shared session: canvas, camera, framing, minimap.
237
+ const view = new StageView(ctx.controller);
238
+ view.version = this.#version;
239
+ stage.view = view;
240
+ stage.setVersion(this.#version);
241
+ release(ctx.controller.addView(view));
242
+ // The stage adds its own minimap to the view (unless a slotted one replaces it).
243
+ view.attach(stage.canvasHost, null);
244
+ view.setChrome(() => stage.chromeLayout());
245
+ this.#setView(view);
246
+ release(() => {
247
+ this.#setView(null);
248
+ view.setChrome(null);
249
+ view.detach();
250
+ });
251
+ // The graph takes keyboard focus (shortcuts apply where focus is), also on click.
252
+ const canvas = stage.canvasHost.querySelector('canvas');
253
+ if (canvas) {
254
+ canvas.tabIndex = 0;
255
+ const focusCanvas = (): void => canvas.focus({ preventScroll: true });
256
+ canvas.addEventListener('pointerdown', focusCanvas);
257
+ release(() => canvas.removeEventListener('pointerdown', focusCanvas));
258
+ }
259
+ const hover = (over: boolean) => (e: PointerEvent) => {
260
+ if (e.pointerType !== 'touch') ctx.hover(over);
261
+ };
262
+ stage.el.addEventListener('pointerenter', hover(true));
263
+ stage.el.addEventListener('pointerleave', hover(false));
264
+ release(() => ctx.hover(false));
265
+ if (typeof ResizeObserver === 'function') {
266
+ const ro = new ResizeObserver(() => {
267
+ stage.layout();
268
+ view.layoutChanged();
269
+ });
270
+ ro.observe(stage.el);
271
+ release(() => ro.disconnect());
272
+ }
273
+ document.fonts?.ready?.then(() => this.#stage?.layout()).catch(() => undefined);
274
+ return stage;
275
+ }
276
+ }
277
+
278
+ define('kgdiff-stage', KgdiffStage);
279
+
280
+ declare global {
281
+ interface HTMLElementTagNameMap {
282
+ /** @experimental In 0.6. */
283
+ 'kgdiff-stage': KgdiffStageElement;
284
+ }
285
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `<kgdiff-title>` (RFC 0001 §3), documented on its class. Importing this module defines it.
3
+ */
4
+ import type { Region } from '../bind.js';
5
+ import type { KgdiffBoundElement } from '../index.js';
6
+ import type { UiContext } from '../ui/context.js';
7
+ import { Title } from '../ui/header.js';
8
+ import { BoundElement, define } from './bound.js';
9
+
10
+ /**
11
+ * The kgdiff mark and the root's compared names and versions. Binds to a root (`for`, `htmlFor`,
12
+ * `rootElement`). Slotted into a `<kgdiff-viewer>` it replaces the title over its canvas.
13
+ *
14
+ * @tagname kgdiff-title
15
+ * @experimental In 0.6; stable from 0.7 (RFC 0001, decision 7).
16
+ */
17
+ export class KgdiffTitle extends BoundElement implements KgdiffBoundElement {
18
+ protected readonly name = 'title';
19
+
20
+ protected build(ui: UiContext): Region {
21
+ const title = new Title(ui);
22
+ this.shadow.append(title.el);
23
+ return title;
24
+ }
25
+ }
26
+
27
+ define('kgdiff-title', KgdiffTitle);
28
+
29
+ declare global {
30
+ interface HTMLElementTagNameMap {
31
+ /** @experimental In 0.6. */
32
+ 'kgdiff-title': KgdiffBoundElement;
33
+ }
34
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `<kgdiff-transport>` (RFC 0001 §3), documented on its class. Importing this module defines it.
3
+ */
4
+ import type { Region } from '../bind.js';
5
+ import { h } from '../dom.js';
6
+ import type { KgdiffBoundElement } from '../index.js';
7
+ import type { UiContext } from '../ui/context.js';
8
+ import { Transport } from '../ui/transport.js';
9
+ import { BoundElement, define } from './bound.js';
10
+
11
+ /**
12
+ * The replay transport of the root's diff. Previous, play / pause, next, the scrubber with one tick
13
+ * per change, the position and the speed; at the end, "Replay finished · Restart" above it. Shown
14
+ * in replay mode only. Binds to a root (`for`, `htmlFor`, `rootElement`). Slotted into a
15
+ * `<kgdiff-stage>` (`slot="bottom-center"`, say) it replaces the stage's built-in transport; placed
16
+ * anywhere else it is not kept clear of fits and flights unless registered. Importing this module
17
+ * defines it.
18
+ *
19
+ * @tagname kgdiff-transport
20
+ * @experimental In 0.6; stable from 0.7 (RFC 0001, decision 7).
21
+ */
22
+ export class KgdiffTransport extends BoundElement implements KgdiffBoundElement {
23
+ protected readonly name = 'transport';
24
+
25
+ protected build(ui: UiContext): Region {
26
+ const transport = new Transport(ui);
27
+ this.shadow.append(h('div', { class: 'stack' }, transport.finished, transport.el));
28
+ return transport;
29
+ }
30
+ }
31
+
32
+ define('kgdiff-transport', KgdiffTransport);
33
+
34
+ declare global {
35
+ interface HTMLElementTagNameMap {
36
+ /** @experimental In 0.6. */
37
+ 'kgdiff-transport': KgdiffBoundElement;
38
+ }
39
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * What the chrome that acts on one view shares (`<kgdiff-zoom>`, `<kgdiff-minimap>`; RFC 0001
3
+ * §3): it binds to a stage, not to a root. The nearest `<kgdiff-stage>` around it (across
4
+ * shadow roots, through the stage context `kgdiff.stage/1`), or the one its `stage="<id>"`
5
+ * names anywhere on the page (a `<kgdiff-viewer>` is the stage of what it holds, and can be
6
+ * named too); a stage defined or bound later is found then. It shows what the
7
+ * stage's root says (language, tokens), acts on the stage's view, and follows the stage when it
8
+ * binds again (a new view).
9
+ */
10
+ import type { Region } from '../bind.js';
11
+ import type { KgdiffRootElement, KgdiffStageElement, KgdiffViewerElement } from '../index.js';
12
+ import type { ElementName } from '../styles.js';
13
+ import type { UiContext } from '../ui/context.js';
14
+ import type { StageView } from '../view.js';
15
+ import { bindContext, mount } from './bound.js';
16
+ import { type StageViewContext, stageContext } from './stage-context.js';
17
+
18
+ /** @experimental The base of the chrome elements that act on a stage's view, in 0.6. */
19
+ export abstract class ViewBoundElement extends HTMLElement {
20
+ static get observedAttributes(): readonly string[] {
21
+ return ['stage'];
22
+ }
23
+
24
+ protected readonly shadow: ShadowRoot;
25
+ #stage: StageViewContext | null = null;
26
+ /** Undone when the element unbinds from its stage. */
27
+ readonly #release: (() => void)[] = [];
28
+ /** Undone when the stage's view or root changes (the region is built again). */
29
+ readonly #regionRelease: (() => void)[] = [];
30
+
31
+ constructor() {
32
+ super();
33
+ this.shadow = this.attachShadow({ mode: 'open' });
34
+ }
35
+
36
+ /** The element's name after `kgdiff-` (its host rules). */
37
+ protected abstract readonly name: ElementName;
38
+
39
+ /** Build the region in the shadow root, for `view`; `release` collects what to undo. */
40
+ protected abstract build(
41
+ ui: UiContext,
42
+ view: StageView,
43
+ release: (fn: () => void) => void,
44
+ ): Region;
45
+
46
+ /**
47
+ * Reflects the `stage` attribute: the id of the stage to act on; '' (no attribute) for the
48
+ * nearest one around the element. Changing it binds again.
49
+ */
50
+ get stage(): string {
51
+ return this.getAttribute('stage') ?? '';
52
+ }
53
+
54
+ set stage(value: string) {
55
+ const id = String(value);
56
+ if (id === '') this.removeAttribute('stage');
57
+ else this.setAttribute('stage', id);
58
+ }
59
+
60
+ /** The stage this element acts on, or null while it waits for one. */
61
+ get stageElement(): KgdiffStageElement | KgdiffViewerElement | null {
62
+ return this.#stage?.stage ?? null;
63
+ }
64
+
65
+ /** The root whose session the stage shows, or null while either waits. */
66
+ get rootElement(): KgdiffRootElement | null {
67
+ return (this.#stage?.root?.host as KgdiffRootElement | undefined) ?? null;
68
+ }
69
+
70
+ connectedCallback(): void {
71
+ // Set before the element was defined: it shadows the accessor.
72
+ if (Object.hasOwn(this, 'stage')) {
73
+ const value = (this as unknown as Record<string, unknown>).stage;
74
+ delete (this as unknown as Record<string, unknown>).stage;
75
+ this.stage = String(value);
76
+ }
77
+ this.#bind();
78
+ }
79
+
80
+ disconnectedCallback(): void {
81
+ this.#unbind();
82
+ }
83
+
84
+ attributeChangedCallback(name: string, old: string | null, value: string | null): void {
85
+ if (name === 'stage' && old !== value && this.isConnected) {
86
+ this.#unbind();
87
+ this.#bind();
88
+ }
89
+ }
90
+
91
+ #bind(): void {
92
+ const cancel = bindContext(this, stageContext, this.getAttribute('stage'), (ctx) => {
93
+ const stage = ctx as StageViewContext;
94
+ this.#stage = stage;
95
+ this.#release.push(stage.watchView((view) => this.#build(view)));
96
+ });
97
+ this.#release.push(cancel);
98
+ }
99
+
100
+ /** The stage has a view (again), or lost it: show the region for it. */
101
+ #build(view: StageView | null): void {
102
+ for (const fn of this.#regionRelease.splice(0)) fn();
103
+ this.shadow.replaceChildren();
104
+ const root = this.#stage?.root;
105
+ if (!view || !root) return;
106
+ const release = (fn: () => void): void => {
107
+ this.#regionRelease.push(fn);
108
+ };
109
+ mount(this, this.shadow, this.name, root, (ui) => this.build(ui, view, release), release);
110
+ }
111
+
112
+ #unbind(): void {
113
+ this.#stage = null;
114
+ for (const fn of this.#regionRelease.splice(0)) fn();
115
+ for (const fn of this.#release.splice(0)) fn();
116
+ this.shadow.replaceChildren();
117
+ }
118
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `<kgdiff-view-dock>` (RFC 0001 §3), documented on its class. Importing this module defines it.
3
+ */
4
+ import type { Region } from '../bind.js';
5
+ import type { KgdiffBoundElement } from '../index.js';
6
+ import type { UiContext } from '../ui/context.js';
7
+ import { Dock } from '../ui/dock.js';
8
+ import { BoundElement, define } from './bound.js';
9
+
10
+ /**
11
+ * How the root's diff is viewed. The mode (overlay, swipe, replay; keys `1`–`3`) and "Changes only"
12
+ * (key `4`) with its context hops and the nearest "+N" expansion. Binds to a root like the other
13
+ * elements (`for`, `htmlFor`, `rootElement`). Slotted into a `<kgdiff-stage>` it replaces the
14
+ * stage's built-in dock; placed anywhere else it is not kept clear of fits and flights unless
15
+ * registered (`stage.registerObstacle`).
16
+ *
17
+ * @tagname kgdiff-view-dock
18
+ * @experimental In 0.6; stable from 0.7 (RFC 0001, decision 7).
19
+ */
20
+ export class KgdiffViewDock extends BoundElement implements KgdiffBoundElement {
21
+ protected readonly name = 'view-dock';
22
+
23
+ protected build(ui: UiContext, _ctx: unknown, release: (fn: () => void) => void): Region {
24
+ const dock = new Dock(ui);
25
+ this.shadow.append(dock.el);
26
+ if (typeof ResizeObserver === 'function') {
27
+ // Narrow docks drop their labels.
28
+ const ro = new ResizeObserver(() => dock.layout());
29
+ ro.observe(this);
30
+ release(() => ro.disconnect());
31
+ }
32
+ return dock;
33
+ }
34
+ }
35
+
36
+ define('kgdiff-view-dock', KgdiffViewDock);
37
+
38
+ declare global {
39
+ interface HTMLElementTagNameMap {
40
+ /** @experimental In 0.6. */
41
+ 'kgdiff-view-dock': KgdiffBoundElement;
42
+ }
43
+ }