@kgdiff/viewer 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/context.d.ts +42 -0
  2. package/dist/context.d.ts.map +1 -0
  3. package/dist/context.js +87 -0
  4. package/dist/context.js.map +1 -0
  5. package/dist/controller.d.ts +26 -185
  6. package/dist/controller.d.ts.map +1 -1
  7. package/dist/controller.js +45 -1155
  8. package/dist/controller.js.map +1 -1
  9. package/dist/element.d.ts +6 -3
  10. package/dist/element.d.ts.map +1 -1
  11. package/dist/element.js +49 -229
  12. package/dist/element.js.map +1 -1
  13. package/dist/index.d.ts +3 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/pipeline.d.ts.map +1 -1
  17. package/dist/pipeline.js +3 -2
  18. package/dist/pipeline.js.map +1 -1
  19. package/dist/root.d.ts +54 -0
  20. package/dist/root.d.ts.map +1 -0
  21. package/dist/root.js +265 -0
  22. package/dist/root.js.map +1 -0
  23. package/dist/schedule.d.ts +22 -0
  24. package/dist/schedule.d.ts.map +1 -0
  25. package/dist/schedule.js +41 -0
  26. package/dist/schedule.js.map +1 -0
  27. package/dist/session.d.ts +253 -0
  28. package/dist/session.d.ts.map +1 -0
  29. package/dist/session.js +1239 -0
  30. package/dist/session.js.map +1 -0
  31. package/dist/standalone/kgdiff-viewer-compute.js +1 -1
  32. package/dist/standalone/kgdiff-viewer.js +4 -4
  33. package/dist/styles.d.ts +13 -6
  34. package/dist/styles.d.ts.map +1 -1
  35. package/dist/styles.js +217 -248
  36. package/dist/styles.js.map +1 -1
  37. package/dist/theme.d.ts +25 -0
  38. package/dist/theme.d.ts.map +1 -0
  39. package/dist/theme.js +149 -0
  40. package/dist/theme.js.map +1 -0
  41. package/dist/ui/change-list.d.ts +18 -0
  42. package/dist/ui/change-list.d.ts.map +1 -0
  43. package/dist/ui/change-list.js +331 -0
  44. package/dist/ui/change-list.js.map +1 -0
  45. package/dist/ui/context.d.ts +19 -5
  46. package/dist/ui/context.d.ts.map +1 -1
  47. package/dist/ui/context.js.map +1 -1
  48. package/dist/ui/filters.d.ts +9 -0
  49. package/dist/ui/filters.d.ts.map +1 -0
  50. package/dist/ui/filters.js +68 -0
  51. package/dist/ui/filters.js.map +1 -0
  52. package/dist/ui/header.js +2 -2
  53. package/dist/ui/header.js.map +1 -1
  54. package/dist/ui/search.d.ts +13 -0
  55. package/dist/ui/search.d.ts.map +1 -0
  56. package/dist/ui/search.js +191 -0
  57. package/dist/ui/search.js.map +1 -0
  58. package/dist/ui/sidebar.d.ts +8 -16
  59. package/dist/ui/sidebar.d.ts.map +1 -1
  60. package/dist/ui/sidebar.js +15 -539
  61. package/dist/ui/sidebar.js.map +1 -1
  62. package/dist/ui/stage.js +1 -1
  63. package/dist/ui/stage.js.map +1 -1
  64. package/dist/ui/swatch.js +1 -1
  65. package/dist/ui/swatch.js.map +1 -1
  66. package/package.json +6 -6
  67. package/src/context.ts +106 -0
  68. package/src/controller.ts +70 -1257
  69. package/src/element.ts +52 -243
  70. package/src/index.ts +3 -1
  71. package/src/pipeline.ts +4 -1
  72. package/src/root.ts +308 -0
  73. package/src/schedule.ts +43 -0
  74. package/src/session.ts +1386 -0
  75. package/src/styles.ts +225 -253
  76. package/src/theme.ts +167 -0
  77. package/src/ui/change-list.ts +401 -0
  78. package/src/ui/context.ts +21 -5
  79. package/src/ui/filters.ts +79 -0
  80. package/src/ui/header.ts +2 -2
  81. package/src/ui/search.ts +220 -0
  82. package/src/ui/sidebar.ts +28 -668
  83. package/src/ui/stage.ts +1 -1
  84. package/src/ui/swatch.ts +1 -1
package/src/element.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  /**
2
- * `<kgdiff-viewer>`: attribute / property reflection, lifecycle, public methods and events
3
- * around a `ViewerController` and the Shadow-DOM UI.
2
+ * `<kgdiff-viewer>`, the default layout (RFC 0001 §4): a root (the session, keyboard map, live
3
+ * region, events and theme tokens; `root.ts`) and the regions bound to it, the stage with its
4
+ * chrome, the change list, the search, the family filters and the detail panel, placed as they
5
+ * always were (a 360 px sidebar; a bottom sheet below 760 px). Around them: attribute /
6
+ * property reflection, lifecycle, the public methods and deep links.
4
7
  */
5
8
  import { type Delta, KgdiffError, type Locale } from '@kgdiff/core';
6
9
  import type { PaletteName } from '@kgdiff/scene';
7
- import { type ControllerHost, type Topic, ViewerController } from './controller.js';
10
+ import type { ViewerController } from './controller.js';
8
11
  import { h } from './dom.js';
9
12
  import { formatHash, parseHash } from './hash-sync.js';
10
13
  import { detectLocale, translate, translator } from './i18n.js';
@@ -20,12 +23,10 @@ import type {
20
23
  ViewerMode,
21
24
  ViewerScope,
22
25
  } from './index.js';
23
- import { isTypingTarget, keyAction } from './keyboard.js';
24
26
  import { assertPayload } from './payload.js';
25
27
  import {
26
28
  DEFAULT_LOOP_DELAY,
27
29
  LEGACY_CHANGES_MODE,
28
- THEME_PREFERENCES,
29
30
  toChrome,
30
31
  toDelay,
31
32
  toLegend,
@@ -35,11 +36,15 @@ import {
35
36
  toScope,
36
37
  toTheme,
37
38
  } from './prefs.js';
38
- import { defaultStorage } from './review-store.js';
39
- import { STYLES, themeVariables } from './styles.js';
40
- import type { UiContext } from './ui/context.js';
39
+ import { bindRegion, uiContext, ViewerRoot } from './root.js';
40
+ import { ALL_TOPICS, type Topic } from './session.js';
41
+ import { ChangeList } from './ui/change-list.js';
42
+ import type { UiActions } from './ui/context.js';
43
+ import { Detail } from './ui/detail.js';
44
+ import { Filters } from './ui/filters.js';
41
45
  import { Header } from './ui/header.js';
42
46
  import { HelpDialog, Menu } from './ui/popovers.js';
47
+ import { Search } from './ui/search.js';
43
48
  import { Sidebar } from './ui/sidebar.js';
44
49
  import { Stage } from './ui/stage.js';
45
50
 
@@ -101,31 +106,6 @@ function deepFreeze<T>(value: T): T {
101
106
  return value;
102
107
  }
103
108
 
104
- function media(query: string): MediaQueryList | null {
105
- return typeof window !== 'undefined' && typeof window.matchMedia === 'function'
106
- ? window.matchMedia(query)
107
- : null;
108
- }
109
-
110
- /**
111
- * Resolve once the browser had a chance to paint the loading step. Hidden tabs never run
112
- * animation frames, so a short timer resolves it there (loading must never stall).
113
- */
114
- function nextPaint(): Promise<void> {
115
- return new Promise((resolve) => {
116
- let done = false;
117
- const finish = (): void => {
118
- if (done) return;
119
- done = true;
120
- resolve();
121
- };
122
- if (typeof requestAnimationFrame === 'function') {
123
- requestAnimationFrame(() => setTimeout(finish, 0));
124
- }
125
- setTimeout(finish, 50);
126
- });
127
- }
128
-
129
109
  export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
130
110
  static get observedAttributes(): readonly string[] {
131
111
  return OBSERVED;
@@ -134,18 +114,17 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
134
114
  /** @internal */
135
115
  readonly controller: ViewerController;
136
116
  readonly #shadow: ShadowRoot;
117
+ readonly #root: ViewerRoot;
137
118
  #ui: {
138
119
  root: HTMLElement;
139
- vars: HTMLStyleElement;
140
120
  header: Header;
141
121
  stage: Stage;
122
+ list: ChangeList;
142
123
  sidebar: Sidebar;
143
124
  menu: Menu;
144
125
  help: HelpDialog;
145
- live: HTMLElement;
146
126
  } | null = null;
147
127
  #currentPayload: ReportPayload | null = null;
148
- #renderFailed = false;
149
128
  #reflecting = false;
150
129
  #compact = false;
151
130
  readonly #cleanup: (() => void)[] = [];
@@ -157,34 +136,13 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
157
136
  constructor() {
158
137
  super();
159
138
  this.#shadow = this.attachShadow({ mode: 'open' });
160
- const host: ControllerHost = {
161
- ready: (stats) => this.#emit('kgdiff-ready', { stats }),
162
- select: (hunk, node) => {
163
- this.#emit('kgdiff-select', { hunk, node });
164
- this.#writeHash();
165
- },
166
- mode: (mode, scope) => {
167
- this.#emit('kgdiff-mode', { mode, scope });
168
- this.#writeHash();
169
- },
170
- review: (hunk, reviewed, progress) =>
171
- this.#emit('kgdiff-review', { hunk, reviewed, progress }),
172
- error: (error) => this.#emit('kgdiff-error', { error }),
173
- announce: (text) => this.#announce(text),
174
- replayEnd: (loop) => this.#emit('kgdiff-replay-end', { loop }),
175
- };
176
- this.controller = new ViewerController(host, {
177
- now: () => (typeof performance !== 'undefined' ? performance.now() : Date.now()),
178
- requestFrame: (cb) =>
179
- typeof requestAnimationFrame === 'function' ? requestAnimationFrame(cb) : 0,
180
- cancelFrame: (id) => {
181
- if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(id);
182
- },
183
- yieldToPaint: nextPaint,
184
- storage: defaultStorage(),
139
+ this.#root = new ViewerRoot(this, this.#shadow, this.#actions(), (type) => {
140
+ if (type === 'kgdiff-select' || type === 'kgdiff-mode') this.#writeHash();
185
141
  });
142
+ this.controller = this.#root.controller;
186
143
  this.controller.locale = toLocale(this.getAttribute('locale')) ?? detectLocale();
187
- this.controller.subscribe((topics) => this.#render(topics));
144
+ // The layout renders first, then the regions in the order they bound to the root.
145
+ this.#root.attach({ update: (topics) => this.#update(topics) });
188
146
  }
189
147
 
190
148
  // ─── Lifecycle ────────────────────────────────────────────────────────────
@@ -201,6 +159,8 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
201
159
  }
202
160
  }
203
161
  }
162
+ // Provide the session (the regions bind to it) and follow the system's preferences.
163
+ this.#cleanup.push(this.#root.connect());
204
164
  if (!this.#ui) this.#build();
205
165
  connected.push(this);
206
166
  const ui = this.#ui;
@@ -216,36 +176,17 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
216
176
  canvas.addEventListener('pointerdown', focusCanvas);
217
177
  this.#cleanup.push(() => canvas.removeEventListener('pointerdown', focusCanvas));
218
178
  }
219
- this.#watchMedia();
220
179
  this.#watchSize();
221
180
  const onKey = (e: KeyboardEvent): void => this.#onKeyDown(e);
222
181
  document.addEventListener('keydown', onKey);
223
182
  this.#cleanup.push(() => document.removeEventListener('keydown', onKey));
224
- const onStorage = (e: StorageEvent): void => this.controller.syncReview(e.key);
225
- window.addEventListener('storage', onStorage);
226
- this.#cleanup.push(() => window.removeEventListener('storage', onStorage));
227
183
  const onHash = (): void => this.#readHash();
228
184
  window.addEventListener('hashchange', onHash);
229
185
  this.#cleanup.push(() => window.removeEventListener('hashchange', onHash));
230
186
  this.controller.setChrome(() => ui.stage.chromeLayout());
231
187
  document.fonts?.ready?.then(() => this.#ui?.stage.layout()).catch(() => undefined);
232
188
  this.#readHash();
233
- this.#render(
234
- new Set<Topic>([
235
- 'status',
236
- 'data',
237
- 'mode',
238
- 'theme',
239
- 'locale',
240
- 'filters',
241
- 'review',
242
- 'selection',
243
- 'focus',
244
- 'swipe',
245
- 'replay',
246
- 'overview',
247
- ]),
248
- );
189
+ this.#root.render(ALL_TOPICS);
249
190
  }
250
191
 
251
192
  disconnectedCallback(): void {
@@ -513,10 +454,6 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
513
454
 
514
455
  // ─── Internals ────────────────────────────────────────────────────────────
515
456
 
516
- #emit(type: string, detail: unknown): void {
517
- this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
518
- }
519
-
520
457
  #reflect(name: string, value: string | null): void {
521
458
  if (this.getAttribute(name) === value) return;
522
459
  this.#reflecting = true;
@@ -528,16 +465,6 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
528
465
  }
529
466
  }
530
467
 
531
- #announce(text: string): void {
532
- const live = this.#ui?.live;
533
- if (!live) return;
534
- live.textContent = '';
535
- // A fresh text node in a later task makes screen readers announce repeats too.
536
- setTimeout(() => {
537
- live.textContent = text;
538
- }, 30);
539
- }
540
-
541
468
  get #sidebarOpen(): boolean {
542
469
  return this.getAttribute('sidebar') !== 'closed';
543
470
  }
@@ -577,19 +504,12 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
577
504
  this.controller.hold(this.#pointerOver && this.loop && this.pauseOnHover);
578
505
  }
579
506
 
580
- #build(): void {
581
- const style = h('style', { text: STYLES });
582
- const vars = h('style');
583
- const ctx: UiContext = {
584
- controller: this.controller,
507
+ /** What the layout does for the regions: preferences reflect as attributes, panels open. */
508
+ #actions(): UiActions {
509
+ return {
585
510
  tr: () => translator(this.controller.locale),
586
- root: this.#shadow,
587
- setMode: (mode) => {
588
- this.controller.setMode(mode);
589
- },
590
- toggleScope: () => {
591
- this.controller.toggleScope();
592
- },
511
+ setMode: (mode) => this.controller.setMode(mode),
512
+ toggleScope: () => this.controller.toggleScope(),
593
513
  setThemePreference: (pref) => {
594
514
  this.theme = pref;
595
515
  },
@@ -605,24 +525,27 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
605
525
  openHelp: () => this.#ui?.help.show(),
606
526
  focusSearch: () => this.#focusSearch(),
607
527
  };
608
- const header = new Header(ctx);
609
- const stage = new Stage(ctx);
610
- const sidebar = new Sidebar(ctx);
611
- const menu = new Menu(ctx);
612
- const help = new HelpDialog(ctx);
613
- const live = h('div', { class: 'sr-only', 'aria-live': 'polite', 'aria-atomic': 'true' });
528
+ }
529
+
530
+ #build(): void {
531
+ const root = h('div', { class: 'root' });
532
+ this.#shadow.append(root);
533
+ // Each region binds to the root on its own (context protocol); the layout only places them.
534
+ const header = bindRegion(root, (ui) => new Header(ui));
535
+ const list = bindRegion(root, (ui) => new ChangeList(ui));
536
+ const search = bindRegion(root, (ui) => new Search(ui));
537
+ const filters = bindRegion(root, (ui) => new Filters(ui));
538
+ const detail = bindRegion(root, (ui) => new Detail(ui));
539
+ const stage = bindRegion(root, (ui) => new Stage(ui));
540
+ const ui = uiContext(this.#root, this.#shadow);
541
+ const menu = new Menu(ui);
542
+ const help = new HelpDialog(ui);
543
+ const sidebar = new Sidebar(ui.tr, list, search, filters, detail);
614
544
  // The title and the actions float over the canvas; the menu drops from the actions.
615
545
  header.attachMenu(menu.el);
616
546
  stage.el.appendChild(header.el);
617
- const root = h(
618
- 'div',
619
- { class: 'root' },
620
- h('div', { class: 'body' }, stage.el, sidebar.el),
621
- help.el,
622
- live,
623
- );
624
- this.#shadow.append(style, vars, root);
625
- this.#ui = { root, vars, header, stage, sidebar, menu, help, live };
547
+ root.append(h('div', { class: 'body' }, stage.el, sidebar.el), help.el, this.#root.live);
548
+ this.#ui = { root, header, stage, list, sidebar, menu, help };
626
549
  // Close the menu on outside clicks.
627
550
  root.addEventListener('pointerdown', (e) => {
628
551
  if (!menu.open) return;
@@ -640,24 +563,8 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
640
563
  this.#applyChrome();
641
564
  }
642
565
 
643
- /** Render, never throwing: a failure shows the error state with safe appearance defaults. */
644
- #render(topics: ReadonlySet<Topic>): void {
645
- try {
646
- this.#renderTopics(topics);
647
- this.#renderFailed = false;
648
- } catch (error) {
649
- console.error('[kgdiff] render failed:', error);
650
- if (this.#renderFailed) return;
651
- this.#renderFailed = true;
652
- const c = this.controller;
653
- c.explicit.clear();
654
- c.setThemePreference('auto');
655
- c.setPalette('default');
656
- c.fail(error instanceof Error ? error : new Error(String(error)));
657
- }
658
- }
659
-
660
- #renderTopics(topics: ReadonlySet<Topic>): void {
566
+ /** The layout's part of a render (the root renders the regions after it). */
567
+ #update(topics: ReadonlySet<Topic>): void {
661
568
  const ui = this.#ui;
662
569
  const c = this.controller;
663
570
  if (topics.has('mode')) {
@@ -671,19 +578,14 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
671
578
  );
672
579
  }
673
580
  if (!ui) return;
674
- if (topics.has('theme') || topics.has('status')) {
675
- ui.vars.textContent = `:host { ${themeVariables(c.theme)} }`;
676
- ui.root.dataset.theme = c.themeName;
677
- }
581
+ if (topics.has('theme') || topics.has('status')) ui.root.dataset.theme = c.themeName;
678
582
  if (topics.has('locale') || topics.has('status')) {
679
583
  ui.root.setAttribute('lang', c.locale);
680
584
  if (ui.menu.open) ui.menu.render();
681
585
  if (ui.help.open) ui.help.render();
682
586
  }
683
587
  if (topics.has('theme') && ui.menu.open) ui.menu.render();
684
- ui.header.update(topics);
685
588
  ui.sidebar.update(topics);
686
- ui.stage.update(topics);
687
589
  if (topics.has('mode') || topics.has('status')) {
688
590
  ui.root.dataset.mode = c.mode;
689
591
  ui.root.dataset.scope = c.scope;
@@ -707,22 +609,7 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
707
609
 
708
610
  #focusSearch(): void {
709
611
  if (!this.#sidebarOpen) this.#toggleSidebar(true);
710
- this.#ui?.sidebar.focusSearch();
711
- }
712
-
713
- #watchMedia(): void {
714
- const dark = media('(prefers-color-scheme: light)');
715
- const motion = media('(prefers-reduced-motion: reduce)');
716
- const apply = (): void => {
717
- this.controller.setSystemDark(!(dark?.matches ?? false));
718
- this.controller.setReducedMotion(motion?.matches ?? false);
719
- };
720
- apply();
721
- for (const q of [dark, motion]) {
722
- if (!q) continue;
723
- q.addEventListener?.('change', apply);
724
- this.#cleanup.push(() => q.removeEventListener?.('change', apply));
725
- }
612
+ this.#root.focus('search');
726
613
  }
727
614
 
728
615
  #watchSize(): void {
@@ -738,7 +625,7 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
738
625
  ui.root.dataset.compact = String(compact);
739
626
  }
740
627
  ui.stage.layout();
741
- ui.sidebar.renderWindow();
628
+ ui.list.renderWindow();
742
629
  if (compact) ui.sidebar.syncSheet();
743
630
  this.controller.layoutChanged();
744
631
  });
@@ -769,85 +656,7 @@ export class KgdiffViewer extends HTMLElement implements KgdiffViewerElement {
769
656
  }
770
657
  return;
771
658
  }
772
- const target = path[0] ?? null;
773
- if (isTypingTarget(target)) return;
774
- const c = this.controller;
775
- const action = keyAction(e, c.mode, c.scope);
776
- if (!action) return;
777
- // Enter / Space on a focused control activate that control.
778
- const el = target as Element | null;
779
- if (
780
- (e.key === 'Enter' || e.key === ' ') &&
781
- el?.closest?.('button, a, summary, [role="radio"], [role="menuitemradio"]')
782
- ) {
783
- return;
784
- }
785
- if (c.status !== 'ready' && !['help', 'theme', 'sidebar', 'escape'].includes(action.type))
786
- return;
787
- e.preventDefault();
788
- switch (action.type) {
789
- case 'mode':
790
- c.setMode(action.mode);
791
- break;
792
- case 'scope':
793
- c.toggleScope();
794
- break;
795
- case 'next':
796
- c.next();
797
- break;
798
- case 'prev':
799
- c.prev();
800
- break;
801
- case 'first':
802
- c.first();
803
- break;
804
- case 'last':
805
- c.last();
806
- break;
807
- case 'fly':
808
- c.flyToSelection();
809
- break;
810
- case 'escape':
811
- if (!ui.sidebar.escape()) c.clearSelection();
812
- break;
813
- case 'play-pause':
814
- c.togglePlay();
815
- break;
816
- case 'step':
817
- c.stepReplay(action.direction);
818
- break;
819
- case 'divider':
820
- c.setSplit(c.split + action.delta);
821
- break;
822
- case 'fit':
823
- void c.fit();
824
- break;
825
- case 'zoom':
826
- c.zoom(action.direction);
827
- break;
828
- case 'search':
829
- this.#focusSearch();
830
- break;
831
- case 'review':
832
- c.toggleReviewed();
833
- break;
834
- case 'sidebar':
835
- this.#toggleSidebar();
836
- break;
837
- case 'theme': {
838
- const i = THEME_PREFERENCES.indexOf(c.themePreference);
839
- this.theme = THEME_PREFERENCES[(i + 1) % THEME_PREFERENCES.length] as ThemePreference;
840
- break;
841
- }
842
- case 'help':
843
- ui.help.show();
844
- break;
845
- case 'expand':
846
- c.expandNearest();
847
- break;
848
- default:
849
- action satisfies never;
850
- }
659
+ this.#root.handleKey(e);
851
660
  }
852
661
 
853
662
  // ─── Deep links ───────────────────────────────────────────────────────────
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ import type {
17
17
  GraphInput,
18
18
  KGraph,
19
19
  Locale,
20
+ PackedDiffJSON,
20
21
  } from '@kgdiff/core';
21
22
  import type { ParseOptions } from '@kgdiff/formats';
22
23
  import type { LayoutJSON, LayoutOptions } from '@kgdiff/layout';
@@ -39,7 +40,8 @@ export type ViewerLegend = 'auto' | 'full' | 'mini' | 'none';
39
40
  export interface ReportPayload {
40
41
  readonly format: 'kgdiff.report';
41
42
  readonly version: 1;
42
- readonly diff: DiffJSON;
43
+ /** The diff, packed (`packDiffJSON`, what the CLI writes) or not. */
44
+ readonly diff: DiffJSON | PackedDiffJSON;
43
45
  readonly layout: LayoutJSON;
44
46
  readonly meta: {
45
47
  readonly title?: string;
package/src/pipeline.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  isKGraph,
12
12
  type KGraph,
13
13
  KgdiffError,
14
+ unpackDiffJSON,
14
15
  } from '@kgdiff/core';
15
16
  import { type LayoutOptions, type LayoutResult, layoutFromJSON } from '@kgdiff/layout';
16
17
  import { loadCompute } from './compute-loader.js';
@@ -157,7 +158,9 @@ export async function readPayload(
157
158
  onStep: (step: LoadStep) => Promise<void>,
158
159
  ): Promise<LoadedDiff> {
159
160
  await onStep('match');
160
- const diff = diffFromJSON(payload.diff);
161
+ const json =
162
+ payload.diff.format === 'kgdiff.diff.packed' ? unpackDiffJSON(payload.diff) : payload.diff;
163
+ const diff = diffFromJSON(json);
161
164
  await onStep('layout');
162
165
  const layout = layoutFromJSON(payload.layout);
163
166
  const vb = str(diff.before.meta.version);