xtralab 0.15.1 → 0.15.2

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 (39) hide show
  1. package/README.md +58 -33
  2. package/lib/fileBrowser/commands.d.ts +1 -0
  3. package/lib/fileBrowser/commands.js +46 -1
  4. package/lib/fileBrowser/fileBrowser.js +4 -8
  5. package/lib/fileBrowser/gitStatus.d.ts +16 -5
  6. package/lib/fileBrowser/gitStatus.js +28 -17
  7. package/lib/fileBrowser/widget.d.ts +15 -0
  8. package/lib/fileBrowser/widget.js +13 -0
  9. package/lib/git/commands.d.ts +5 -0
  10. package/lib/git/commands.js +4 -3
  11. package/lib/git/diffPreferences.d.ts +90 -0
  12. package/lib/git/diffPreferences.js +107 -0
  13. package/lib/git/diffProvider.js +10 -2
  14. package/lib/git/diffSurface.d.ts +34 -19
  15. package/lib/git/diffSurface.js +49 -115
  16. package/lib/git/diffTheme.d.ts +7 -0
  17. package/lib/git/diffTheme.js +23 -0
  18. package/lib/git/diffWidget.d.ts +7 -30
  19. package/lib/git/diffWidget.js +56 -100
  20. package/lib/git/imageDiff.d.ts +9 -0
  21. package/lib/git/imageDiff.js +2 -28
  22. package/lib/git/index.js +41 -2
  23. package/lib/git/notebookDiff.js +3 -2
  24. package/package.json +5 -3
  25. package/schema/git-diff-preferences.json +42 -0
  26. package/src/fileBrowser/commands.ts +64 -0
  27. package/src/fileBrowser/fileBrowser.tsx +4 -9
  28. package/src/fileBrowser/gitStatus.ts +33 -20
  29. package/src/fileBrowser/widget.tsx +22 -0
  30. package/src/git/commands.ts +13 -1
  31. package/src/git/diffPreferences.ts +192 -0
  32. package/src/git/diffProvider.tsx +11 -2
  33. package/src/git/diffSurface.tsx +113 -146
  34. package/src/git/diffTheme.ts +24 -0
  35. package/src/git/diffWidget.tsx +108 -138
  36. package/src/git/imageDiff.tsx +12 -34
  37. package/src/git/index.ts +51 -2
  38. package/src/git/notebookDiff.tsx +3 -2
  39. package/style/git.css +4 -2
@@ -0,0 +1,107 @@
1
+ import * as React from 'react';
2
+ import { Token } from '@lumino/coreutils';
3
+ import { Signal } from '@lumino/signaling';
4
+ export const MIN_SPLIT_RATIO = 0.1;
5
+ export const MAX_SPLIT_RATIO = 0.9;
6
+ export const DEFAULT_SPLIT_RATIO = 0.5;
7
+ const SPLIT_RATIO_STATE_KEY = 'xtralab:git-diff:split-ratio';
8
+ export const IDiffPreferences = new Token('xtralab:IDiffPreferences', 'Display choices shared by the git diff views.');
9
+ const SETTING_KEYS = [
10
+ 'diffStyle',
11
+ 'diffStyleControl',
12
+ 'lineWrap',
13
+ 'notebookViewMode',
14
+ 'imageViewMode'
15
+ ];
16
+ /**
17
+ * {@link IDiffPreferences} backed by the settings registry and the state
18
+ * database. Without them it keeps the values in memory only.
19
+ */
20
+ export class DiffPreferences {
21
+ constructor() {
22
+ this._values = {
23
+ diffStyle: 'split',
24
+ diffStyleControl: 'buttons',
25
+ lineWrap: false,
26
+ notebookViewMode: 'notebook',
27
+ imageViewMode: '2-up',
28
+ splitRatio: DEFAULT_SPLIT_RATIO
29
+ };
30
+ this._changed = new Signal(this);
31
+ this._settings = null;
32
+ this._state = null;
33
+ }
34
+ get values() {
35
+ return this._values;
36
+ }
37
+ get changed() {
38
+ return this._changed;
39
+ }
40
+ update(values) {
41
+ var _a, _b;
42
+ if (!this._apply(values)) {
43
+ return;
44
+ }
45
+ for (const key of SETTING_KEYS) {
46
+ if (key in values) {
47
+ (_a = this._settings) === null || _a === void 0 ? void 0 : _a.set(key, this._values[key]).catch(reason => {
48
+ console.error(`xtralab: failed to save the diff ${key}`, reason);
49
+ });
50
+ }
51
+ }
52
+ if ('splitRatio' in values) {
53
+ (_b = this._state) === null || _b === void 0 ? void 0 : _b.save(SPLIT_RATIO_STATE_KEY, this._values.splitRatio).catch(reason => {
54
+ console.error('xtralab: failed to save the diff split', reason);
55
+ });
56
+ }
57
+ }
58
+ /**
59
+ * Read the toggles from the plugin settings and follow their changes.
60
+ */
61
+ connectSettings(settings) {
62
+ this._settings = settings;
63
+ const sync = () => {
64
+ this._apply(settings.composite);
65
+ };
66
+ sync();
67
+ settings.changed.connect(sync);
68
+ }
69
+ /**
70
+ * Restore the split width from the state database and save it there.
71
+ */
72
+ async connectState(state) {
73
+ this._state = state;
74
+ const ratio = await state.fetch(SPLIT_RATIO_STATE_KEY);
75
+ if (typeof ratio === 'number' &&
76
+ ratio >= MIN_SPLIT_RATIO &&
77
+ ratio <= MAX_SPLIT_RATIO) {
78
+ this._apply({ splitRatio: ratio });
79
+ }
80
+ }
81
+ /**
82
+ * Merge values without persisting them; return whether one changed.
83
+ */
84
+ _apply(values) {
85
+ const next = { ...this._values, ...values };
86
+ const keys = Object.keys(next);
87
+ if (keys.every(key => next[key] === this._values[key])) {
88
+ return false;
89
+ }
90
+ this._values = next;
91
+ this._changed.emit();
92
+ return true;
93
+ }
94
+ }
95
+ /**
96
+ * Re-render on every preference change and return the current values.
97
+ */
98
+ export function useDiffPreferences(preferences) {
99
+ const subscribe = React.useCallback((onChange) => {
100
+ const slot = () => onChange();
101
+ preferences.changed.connect(slot);
102
+ return () => {
103
+ preferences.changed.disconnect(slot);
104
+ };
105
+ }, [preferences]);
106
+ return React.useSyncExternalStore(subscribe, () => preferences.values);
107
+ }
@@ -3,6 +3,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
3
3
  import { ITranslator, nullTranslator } from '@jupyterlab/translation';
4
4
  import { IGitExtension } from '@jupyterlab/git';
5
5
  import { IAskAgent } from '../askAgent/tokens';
6
+ import { DiffPreferences, IDiffPreferences } from './diffPreferences';
6
7
  import { XtralabDiffWidget, addDiffToolbarItems } from './diffWidget';
7
8
  import { IMAGE_DIFF_EXTENSIONS } from './imageDiff';
8
9
  /**
@@ -22,14 +23,21 @@ const diffProviderPlugin = {
22
23
  description: "Replaces jupyterlab-git's notebook, text and image diff plugins with xtralab's renderer.",
23
24
  autoStart: true,
24
25
  requires: [IGitExtension],
25
- optional: [IRenderMimeRegistry, IThemeManager, ITranslator, IAskAgent],
26
- activate: (app, gitExtension, rendermime, themeManager, translator, askAgent) => {
26
+ optional: [
27
+ IRenderMimeRegistry,
28
+ IThemeManager,
29
+ ITranslator,
30
+ IAskAgent,
31
+ IDiffPreferences
32
+ ],
33
+ activate: (app, gitExtension, rendermime, themeManager, translator, askAgent, preferences) => {
27
34
  const trans = (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab');
28
35
  const factory = makeXtralabDiffFactory({
29
36
  contentsManager: app.serviceManager.contents,
30
37
  rendermime,
31
38
  themeManager,
32
39
  askAgent,
40
+ preferences: preferences !== null && preferences !== void 0 ? preferences : new DiffPreferences(),
33
41
  trans
34
42
  });
35
43
  // Use one shared factory; the surface detects notebooks/images by filename.
@@ -3,25 +3,8 @@ import { IThemeManager } from '@jupyterlab/apputils';
3
3
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
4
4
  import type { TranslationBundle } from '@jupyterlab/translation';
5
5
  import { type SelectedLineRange } from '@pierre/diffs';
6
+ import { type DiffStyle, type DiffStyleControlMode, type ImageDiffViewMode, type NotebookDiffViewMode } from './diffPreferences';
6
7
  export declare const DIFF_WIDGET_CSS_CLASS = "jp-xtralab-DiffWidget";
7
- export type DiffStyle = 'split' | 'unified';
8
- /**
9
- * Read the persisted diff style, defaulting to `'split'`.
10
- */
11
- export declare function readStoredDiffStyle(): DiffStyle;
12
- /**
13
- * Persist the diff style to local storage (best-effort).
14
- */
15
- export declare function writeStoredDiffStyle(style: DiffStyle): void;
16
- export type NotebookDiffViewMode = 'notebook' | 'json';
17
- /**
18
- * Read the persisted notebook view mode, defaulting to `'notebook'`.
19
- */
20
- export declare function readStoredNotebookViewMode(): NotebookDiffViewMode;
21
- /**
22
- * Persist the notebook view mode to local storage (best-effort).
23
- */
24
- export declare function writeStoredNotebookViewMode(mode: NotebookDiffViewMode): void;
25
8
  /**
26
9
  * Dark-theme check via `IThemeManager`, falling back to the
27
10
  * `data-jp-theme-light` body attribute for hosts without the token.
@@ -101,6 +84,26 @@ interface IDiffSurfaceProps {
101
84
  * Split vs unified layout for the textual file diff (host-controlled).
102
85
  */
103
86
  diffStyle: DiffStyle;
87
+ /**
88
+ * Whether long lines wrap instead of scrolling horizontally (host-controlled).
89
+ */
90
+ lineWrap: boolean;
91
+ /**
92
+ * Left pane width in split view, as a fraction of the diff width (host-controlled).
93
+ */
94
+ splitRatio: number;
95
+ /**
96
+ * Called when the user finishes resizing the split panes.
97
+ */
98
+ onSplitRatioChange: (ratio: number) => void;
99
+ /**
100
+ * 2-up, swipe or onion view for image diffs (host-controlled).
101
+ */
102
+ imageViewMode: ImageDiffViewMode;
103
+ /**
104
+ * Called when the user picks another image view mode.
105
+ */
106
+ onImageViewModeChange: (mode: ImageDiffViewMode) => void;
104
107
  /**
105
108
  * Fires when rendered-notebook availability changes so the host can toggle its Notebook/JSON control.
106
109
  */
@@ -145,13 +148,25 @@ export declare function NotebookViewModeControl(props: {
145
148
  trans: TranslationBundle;
146
149
  }): React.ReactElement;
147
150
  /**
148
- * Segmented Split/Unified selector; mirrors {@link NotebookViewModeControl}.
151
+ * Split/Unified selector: two segmented buttons, or with `control: 'toggle'`
152
+ * one button that shows the layout it switches to.
149
153
  * `available` mirrors the surface's `onFileDiffActiveChange`.
150
154
  */
151
155
  export declare function DiffStyleControl(props: {
152
156
  diffStyle: DiffStyle;
157
+ control: DiffStyleControlMode;
153
158
  available: boolean;
154
159
  onChange: (style: DiffStyle) => void;
155
160
  trans: TranslationBundle;
156
161
  }): React.ReactElement;
162
+ /**
163
+ * Line-wrap toggle styled as a one-button segmented control.
164
+ * `available` mirrors the surface's `onFileDiffActiveChange`.
165
+ */
166
+ export declare function LineWrapControl(props: {
167
+ lineWrap: boolean;
168
+ available: boolean;
169
+ onChange: (wrap: boolean) => void;
170
+ trans: TranslationBundle;
171
+ }): React.ReactElement;
157
172
  export {};
@@ -1,15 +1,13 @@
1
1
  import * as React from 'react';
2
2
  import { undoIcon } from '@jupyterlab/ui-components';
3
3
  import { FileDiff } from '@pierre/diffs/react';
4
+ import { IconDiffSplit, IconDiffUnified, IconWordWrap } from '@pierre/icons';
4
5
  import { diffAcceptRejectHunk, parseDiffFromFile } from '@pierre/diffs';
5
6
  import { ImageDiffView, imageDataType } from './imageDiff';
6
7
  import { buildNotebookDiff, NotebookDiffView } from './notebookDiff';
7
- import { resolveDiffTheme } from './diffTheme';
8
+ import { DEFAULT_SPLIT_RATIO, MAX_SPLIT_RATIO, MIN_SPLIT_RATIO } from './diffPreferences';
9
+ import { DIFF_SCROLLBAR_CSS, resolveDiffTheme } from './diffTheme';
8
10
  export const DIFF_WIDGET_CSS_CLASS = 'jp-xtralab-DiffWidget';
9
- const SPLIT_RATIO_STORAGE_KEY = 'xtralab:diff-split-ratio';
10
- const MIN_SPLIT_RATIO = 0.1;
11
- const MAX_SPLIT_RATIO = 0.9;
12
- const DEFAULT_SPLIT_RATIO = 0.5;
13
11
  /**
14
12
  * Injected via the `unsafeCSS` option into the shadow root's `@layer
15
13
  * unsafe`, beating the library's base rule that hardcodes `1fr 1fr`. Reads
@@ -19,86 +17,7 @@ const DEFAULT_SPLIT_RATIO = 0.5;
19
17
  const SPLIT_RESIZE_CSS = `pre[data-diff-type="split"][data-overflow="scroll"] {
20
18
  grid-template-columns: var(--xtralab-split-cols, 1fr 1fr);
21
19
  }`;
22
- function readStoredSplitRatio() {
23
- try {
24
- const raw = window.localStorage.getItem(SPLIT_RATIO_STORAGE_KEY);
25
- if (raw === null) {
26
- return DEFAULT_SPLIT_RATIO;
27
- }
28
- const parsed = Number.parseFloat(raw);
29
- if (Number.isFinite(parsed) &&
30
- parsed >= MIN_SPLIT_RATIO &&
31
- parsed <= MAX_SPLIT_RATIO) {
32
- return parsed;
33
- }
34
- }
35
- catch (_a) {
36
- // localStorage can throw in privacy mode or sandboxed contexts.
37
- }
38
- return DEFAULT_SPLIT_RATIO;
39
- }
40
- function writeStoredSplitRatio(ratio) {
41
- try {
42
- window.localStorage.setItem(SPLIT_RATIO_STORAGE_KEY, ratio.toString());
43
- }
44
- catch (_a) {
45
- // See readStoredSplitRatio — best-effort persistence.
46
- }
47
- }
48
- const DIFF_STYLE_STORAGE_KEY = 'xtralab:diff-style';
49
- /**
50
- * Read the persisted diff style, defaulting to `'split'`.
51
- */
52
- export function readStoredDiffStyle() {
53
- try {
54
- const raw = window.localStorage.getItem(DIFF_STYLE_STORAGE_KEY);
55
- if (raw === 'split' || raw === 'unified') {
56
- return raw;
57
- }
58
- }
59
- catch (_a) {
60
- // See readStoredSplitRatio.
61
- }
62
- return 'split';
63
- }
64
- /**
65
- * Persist the diff style to local storage (best-effort).
66
- */
67
- export function writeStoredDiffStyle(style) {
68
- try {
69
- window.localStorage.setItem(DIFF_STYLE_STORAGE_KEY, style);
70
- }
71
- catch (_a) {
72
- // Best-effort.
73
- }
74
- }
75
- const NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY = 'xtralab:notebook-diff-view-mode';
76
- /**
77
- * Read the persisted notebook view mode, defaulting to `'notebook'`.
78
- */
79
- export function readStoredNotebookViewMode() {
80
- try {
81
- const raw = window.localStorage.getItem(NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY);
82
- if (raw === 'json' || raw === 'notebook') {
83
- return raw;
84
- }
85
- }
86
- catch (_a) {
87
- // See readStoredSplitRatio.
88
- }
89
- return 'notebook';
90
- }
91
- /**
92
- * Persist the notebook view mode to local storage (best-effort).
93
- */
94
- export function writeStoredNotebookViewMode(mode) {
95
- try {
96
- window.localStorage.setItem(NOTEBOOK_DIFF_VIEW_MODE_STORAGE_KEY, mode);
97
- }
98
- catch (_a) {
99
- // Best-effort.
100
- }
101
- }
20
+ const DIFF_SURFACE_CSS = `${SPLIT_RESIZE_CSS}\n${DIFF_SCROLLBAR_CSS}`;
102
21
  /**
103
22
  * Extension-only check; a false positive is harmless because
104
23
  * {@link buildNotebookDiff} validates the JSON and the file diff is the fallback.
@@ -128,7 +47,7 @@ export function isPierreTheme(themeManager) {
128
47
  * Shared renderer for text, notebook and image diffs.
129
48
  */
130
49
  export function DiffSurface(props) {
131
- const { loading, error, isBinary, oldText, newText, newName, oldName, dark, pierreTheme, rendermime, notebookViewMode, diffStyle, onNotebookAvailabilityChange, onFileDiffActiveChange, onMetadataChange, hunkDiscard, onLineAsk, trans } = props;
50
+ const { loading, error, isBinary, oldText, newText, newName, oldName, dark, pierreTheme, rendermime, notebookViewMode, diffStyle, lineWrap, splitRatio, onSplitRatioChange, imageViewMode, onImageViewModeChange, onNotebookAvailabilityChange, onFileDiffActiveChange, onMetadataChange, hunkDiscard, onLineAsk, trans } = props;
132
51
  const hasContent = !loading && error === null && !isBinary;
133
52
  // For raster images the host passes both sides as base64 in oldText/newText.
134
53
  const imageType = React.useMemo(() => imageDataType(newName), [newName]);
@@ -167,7 +86,11 @@ export function DiffSurface(props) {
167
86
  React.useEffect(() => {
168
87
  onFileDiffActiveChange === null || onFileDiffActiveChange === void 0 ? void 0 : onFileDiffActiveChange(showFileDiff);
169
88
  }, [onFileDiffActiveChange, showFileDiff]);
170
- const [leftRatio, setLeftRatio] = React.useState(() => readStoredSplitRatio());
89
+ // Local while dragging; the host hears the ratio when a drag ends.
90
+ const [leftRatio, setLeftRatio] = React.useState(splitRatio);
91
+ React.useEffect(() => {
92
+ setLeftRatio(splitRatio);
93
+ }, [splitRatio]);
171
94
  const wrapperRef = React.useRef(null);
172
95
  // Drag listeners read the live ratio from a ref to avoid stale captures.
173
96
  const leftRatioRef = React.useRef(leftRatio);
@@ -248,16 +171,16 @@ export function DiffSurface(props) {
248
171
  handle.removeEventListener('pointermove', onPointerMove);
249
172
  handle.removeEventListener('pointerup', onPointerEnd);
250
173
  handle.removeEventListener('pointercancel', onPointerEnd);
251
- writeStoredSplitRatio(leftRatioRef.current);
174
+ onSplitRatioChange(leftRatioRef.current);
252
175
  };
253
176
  handle.addEventListener('pointermove', onPointerMove);
254
177
  handle.addEventListener('pointerup', onPointerEnd);
255
178
  handle.addEventListener('pointercancel', onPointerEnd);
256
- }, []);
179
+ }, [onSplitRatioChange]);
257
180
  const handleResizerDoubleClick = React.useCallback(() => {
258
181
  setLeftRatio(DEFAULT_SPLIT_RATIO);
259
- writeStoredSplitRatio(DEFAULT_SPLIT_RATIO);
260
- }, []);
182
+ onSplitRatioChange(DEFAULT_SPLIT_RATIO);
183
+ }, [onSplitRatioChange]);
261
184
  const handleResizerKeyDown = React.useCallback((event) => {
262
185
  const step = event.shiftKey ? 0.1 : 0.01;
263
186
  let next = null;
@@ -281,8 +204,8 @@ export function DiffSurface(props) {
281
204
  event.preventDefault();
282
205
  const clamped = Math.max(MIN_SPLIT_RATIO, Math.min(MAX_SPLIT_RATIO, next));
283
206
  setLeftRatio(clamped);
284
- writeStoredSplitRatio(clamped);
285
- }, []);
207
+ onSplitRatioChange(clamped);
208
+ }, [onSplitRatioChange]);
286
209
  if (loading) {
287
210
  return (React.createElement("div", { className: "jp-xtralab-DiffWidget-status" }, trans.__('Loading diff…')));
288
211
  }
@@ -294,12 +217,15 @@ export function DiffSurface(props) {
294
217
  }
295
218
  if (imageType !== null) {
296
219
  return (React.createElement("div", { className: "jp-xtralab-DiffWidget-content" },
297
- React.createElement(ImageDiffView, { reference: oldText, challenger: newText, fileType: imageType, trans: trans })));
220
+ React.createElement(ImageDiffView, { reference: oldText, challenger: newText, fileType: imageType, mode: imageViewMode, onModeChange: onImageViewModeChange, trans: trans })));
298
221
  }
299
222
  if (!showNotebookView && !showFileDiff) {
300
223
  return (React.createElement("div", { className: "jp-xtralab-DiffWidget-status" }, trans.__('No content to diff.')));
301
224
  }
302
225
  const leftPercent = leftRatio * 100;
226
+ // The wrapped split grid sizes its gutter tracks to their content, so a
227
+ // percentage split cannot line up with the handle.
228
+ const showResizer = showFileDiff && diffStyle === 'split' && !lineWrap;
303
229
  const hostStyle = {
304
230
  '--xtralab-split-cols': `${leftPercent}% ${100 - leftPercent}%`
305
231
  };
@@ -310,11 +236,12 @@ export function DiffSurface(props) {
310
236
  // at a resolvable URL, so the pool crashes; run on the main thread.
311
237
  disableWorkerPool: true, options: {
312
238
  diffStyle,
239
+ overflow: lineWrap ? 'wrap' : 'scroll',
313
240
  disableFileHeader: true,
314
241
  theme: resolveDiffTheme(dark, pierreTheme),
315
242
  themeType: dark ? 'dark' : 'light',
316
243
  // Constant string lets the library skip its unsafeCSS re-render path.
317
- unsafeCSS: SPLIT_RESIZE_CSS,
244
+ unsafeCSS: DIFF_SURFACE_CSS,
318
245
  ...(onLineAsk !== undefined
319
246
  ? {
320
247
  enableLineSelection: true,
@@ -323,7 +250,7 @@ export function DiffSurface(props) {
323
250
  }
324
251
  : {})
325
252
  } })) : null),
326
- showFileDiff && diffStyle === 'split' ? (React.createElement("div", { className: "jp-xtralab-DiffWidget-resizer", style: { left: `${leftPercent}%` }, role: "separator", "aria-orientation": "vertical", "aria-valuenow": Math.round(leftPercent), "aria-valuemin": Math.round(MIN_SPLIT_RATIO * 100), "aria-valuemax": Math.round(MAX_SPLIT_RATIO * 100), "aria-label": trans.__('Resize the diff panes'), tabIndex: 0, title: trans.__('Drag to resize the diff panes (double-click to reset)'), onPointerDown: handleResizerPointerDown, onDoubleClick: handleResizerDoubleClick, onKeyDown: handleResizerKeyDown })) : null)));
253
+ showResizer ? (React.createElement("div", { className: "jp-xtralab-DiffWidget-resizer", style: { left: `${leftPercent}%` }, role: "separator", "aria-orientation": "vertical", "aria-valuenow": Math.round(leftPercent), "aria-valuemin": Math.round(MIN_SPLIT_RATIO * 100), "aria-valuemax": Math.round(MAX_SPLIT_RATIO * 100), "aria-label": trans.__('Resize the diff panes'), tabIndex: 0, title: trans.__('Drag to resize the diff panes (double-click to reset)'), onPointerDown: handleResizerPointerDown, onDoubleClick: handleResizerDoubleClick, onKeyDown: handleResizerKeyDown })) : null)));
327
254
  }
328
255
  /**
329
256
  * Segmented Notebook/JSON selector. Hosts mount it into their own toolbar
@@ -339,32 +266,39 @@ export function NotebookViewModeControl(props) {
339
266
  React.createElement("button", { type: "button", role: "tab", "aria-selected": mode === 'json', "data-active": mode === 'json', className: "jp-xtralab-DiffWidget-segmentedButton", onClick: () => onChange('json') }, trans.__('JSON'))));
340
267
  }
341
268
  /**
342
- * Split / unified glyphs inlined from the `@pierre/diffs` icon sprite so
343
- * the toggle matches the library's docs site without the sprite sheet;
344
- * `currentColor` lets the button styling drive the fill.
345
- */
346
- function DiffSplitIcon() {
347
- return (React.createElement("svg", { viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true" },
348
- React.createElement("path", { d: "M14 0H8.5v16H14a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2m-1.5 6.5v1h1a.5.5 0 0 1 0 1h-1v1a.5.5 0 0 1-1 0v-1h-1a.5.5 0 0 1 0-1h1v-1a.5.5 0 0 1 1 0" }),
349
- React.createElement("path", { d: "M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h5.5V0zm.5 7.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1", opacity: ".3" })));
350
- }
351
- function DiffUnifiedIcon() {
352
- return (React.createElement("svg", { viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true" },
353
- React.createElement("path", { fillRule: "evenodd", d: "M16 14a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V8.5h16zm-8-4a.5.5 0 0 0-.5.5v1h-1a.5.5 0 0 0 0 1h1v1a.5.5 0 0 0 1 0v-1h1a.5.5 0 0 0 0-1h-1v-1A.5.5 0 0 0 8 10", clipRule: "evenodd" }),
354
- React.createElement("path", { fillRule: "evenodd", d: "M14 0a2 2 0 0 1 2 2v5.5H0V2a2 2 0 0 1 2-2zM6.5 3.5a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1z", clipRule: "evenodd", opacity: ".4" })));
355
- }
356
- /**
357
- * Segmented Split/Unified selector; mirrors {@link NotebookViewModeControl}.
269
+ * Split/Unified selector: two segmented buttons, or with `control: 'toggle'`
270
+ * one button that shows the layout it switches to.
358
271
  * `available` mirrors the surface's `onFileDiffActiveChange`.
359
272
  */
360
273
  export function DiffStyleControl(props) {
361
- const { diffStyle, available, onChange, trans } = props;
274
+ const { diffStyle, control, available, onChange, trans } = props;
362
275
  if (!available) {
363
276
  return React.createElement(React.Fragment, null);
364
277
  }
278
+ if (control === 'toggle') {
279
+ const next = diffStyle === 'split' ? 'unified' : 'split';
280
+ const label = next === 'split'
281
+ ? trans.__('Switch to split view')
282
+ : trans.__('Switch to unified view');
283
+ return (React.createElement("div", { className: "jp-xtralab-DiffWidget-segmented" },
284
+ React.createElement("button", { type: "button", className: "jp-xtralab-DiffWidget-segmentedButton jp-xtralab-DiffWidget-segmentedButton-icon", title: label, "aria-label": label, onClick: () => onChange(next) }, next === 'split' ? React.createElement(IconDiffSplit, null) : React.createElement(IconDiffUnified, null))));
285
+ }
365
286
  return (React.createElement("div", { className: "jp-xtralab-DiffWidget-segmented", role: "tablist", "aria-label": trans.__('Diff view style') },
366
287
  React.createElement("button", { type: "button", role: "tab", "aria-selected": diffStyle === 'split', "data-active": diffStyle === 'split', className: "jp-xtralab-DiffWidget-segmentedButton jp-xtralab-DiffWidget-segmentedButton-icon", title: trans.__('Split (side-by-side) view'), "aria-label": trans.__('Split view'), onClick: () => onChange('split') },
367
- React.createElement(DiffSplitIcon, null)),
288
+ React.createElement(IconDiffSplit, null)),
368
289
  React.createElement("button", { type: "button", role: "tab", "aria-selected": diffStyle === 'unified', "data-active": diffStyle === 'unified', className: "jp-xtralab-DiffWidget-segmentedButton jp-xtralab-DiffWidget-segmentedButton-icon", title: trans.__('Unified (inline) view'), "aria-label": trans.__('Unified view'), onClick: () => onChange('unified') },
369
- React.createElement(DiffUnifiedIcon, null))));
290
+ React.createElement(IconDiffUnified, null))));
291
+ }
292
+ /**
293
+ * Line-wrap toggle styled as a one-button segmented control.
294
+ * `available` mirrors the surface's `onFileDiffActiveChange`.
295
+ */
296
+ export function LineWrapControl(props) {
297
+ const { lineWrap, available, onChange, trans } = props;
298
+ if (!available) {
299
+ return React.createElement(React.Fragment, null);
300
+ }
301
+ return (React.createElement("div", { className: "jp-xtralab-DiffWidget-segmented" },
302
+ React.createElement("button", { type: "button", "aria-pressed": lineWrap, "data-active": lineWrap, className: "jp-xtralab-DiffWidget-segmentedButton jp-xtralab-DiffWidget-segmentedButton-icon", title: trans.__('Wrap long lines'), "aria-label": trans.__('Wrap lines'), onClick: () => onChange(!lineWrap) },
303
+ React.createElement(IconWordWrap, null))));
370
304
  }
@@ -1,4 +1,11 @@
1
1
  import { type DiffsThemeNames } from '@pierre/diffs';
2
+ /**
3
+ * `unsafeCSS` for a taller, always-visible horizontal scrollbar. The library
4
+ * default is 6px and only paints the thumb on hover, in a color close to the
5
+ * background, so users miss that long lines scroll. Same thumb color as the
6
+ * app scrollbars in chrome.css.
7
+ */
8
+ export declare const DIFF_SCROLLBAR_CSS = ":host {\n --diffs-scrollbar-gutter-override: 10px;\n --xtralab-diff-scrollbar-thumb: color-mix(in srgb, var(--jp-ui-font-color1) 28%, transparent);\n}\n[data-code]::-webkit-scrollbar-thumb {\n background-color: var(--xtralab-diff-scrollbar-thumb);\n border-width: 2px;\n border-radius: 999px;\n}\n[data-code]::-webkit-scrollbar-thumb:hover {\n background-color: color-mix(in srgb, var(--jp-ui-font-color1) 45%, transparent);\n}\n@supports (-moz-appearance: none) {\n [data-code] {\n scrollbar-color: var(--xtralab-diff-scrollbar-thumb) transparent;\n }\n}";
2
9
  /**
3
10
  * Choose Pierre's palette or the JupyterLab CSS-variable theme.
4
11
  */
@@ -20,6 +20,29 @@ registerCustomCSSVariableTheme(JUPYTERLAB_DIFF_THEME, {
20
20
  'token-punctuation': 'var(--jp-mirror-editor-punctuation-color)',
21
21
  'token-link': 'var(--jp-mirror-editor-link-color)'
22
22
  });
23
+ /**
24
+ * `unsafeCSS` for a taller, always-visible horizontal scrollbar. The library
25
+ * default is 6px and only paints the thumb on hover, in a color close to the
26
+ * background, so users miss that long lines scroll. Same thumb color as the
27
+ * app scrollbars in chrome.css.
28
+ */
29
+ export const DIFF_SCROLLBAR_CSS = `:host {
30
+ --diffs-scrollbar-gutter-override: 10px;
31
+ --xtralab-diff-scrollbar-thumb: color-mix(in srgb, var(--jp-ui-font-color1) 28%, transparent);
32
+ }
33
+ [data-code]::-webkit-scrollbar-thumb {
34
+ background-color: var(--xtralab-diff-scrollbar-thumb);
35
+ border-width: 2px;
36
+ border-radius: 999px;
37
+ }
38
+ [data-code]::-webkit-scrollbar-thumb:hover {
39
+ background-color: color-mix(in srgb, var(--jp-ui-font-color1) 45%, transparent);
40
+ }
41
+ @supports (-moz-appearance: none) {
42
+ [data-code] {
43
+ scrollbar-color: var(--xtralab-diff-scrollbar-thumb) transparent;
44
+ }
45
+ }`;
23
46
  /**
24
47
  * Choose Pierre's palette or the JupyterLab CSS-variable theme.
25
48
  */
@@ -8,7 +8,8 @@ import { ReactWidget } from '@jupyterlab/ui-components';
8
8
  import { ISignal } from '@lumino/signaling';
9
9
  import { Widget } from '@lumino/widgets';
10
10
  import type { IAskAgent } from '../askAgent/tokens';
11
- import { DIFF_WIDGET_CSS_CLASS, type DiffStyle, type NotebookDiffViewMode } from './diffSurface';
11
+ import { type IDiffPreferences } from './diffPreferences';
12
+ import { DIFF_WIDGET_CSS_CLASS } from './diffSurface';
12
13
  export { DIFF_WIDGET_CSS_CLASS };
13
14
  /**
14
15
  * `Git.Diff.IModel` plus optional xtralab-only metadata.
@@ -49,6 +50,10 @@ export interface IXtralabDiffContext {
49
50
  * When available, diff line selections get an "ask an agent" gutter button.
50
51
  */
51
52
  askAgent: IAskAgent | null;
53
+ /**
54
+ * The display choices shared by all diff views.
55
+ */
56
+ preferences: IDiffPreferences;
52
57
  /**
53
58
  * The application translation bundle.
54
59
  */
@@ -79,18 +84,6 @@ export declare class XtralabDiffWidget extends ReactWidget implements Git.Diff.I
79
84
  * Re-pull both sides of the model and re-render.
80
85
  */
81
86
  refresh(): Promise<void>;
82
- /**
83
- * The current rendered-vs-JSON choice for notebook diffs.
84
- */
85
- get notebookViewMode(): NotebookDiffViewMode;
86
- /**
87
- * Set the notebook view mode and persist it.
88
- */
89
- setNotebookViewMode(mode: NotebookDiffViewMode): void;
90
- /**
91
- * A signal emitted when the notebook view mode changes.
92
- */
93
- get notebookViewModeChanged(): ISignal<this, NotebookDiffViewMode>;
94
87
  /**
95
88
  * Whether a rendered notebook view is currently available.
96
89
  */
@@ -104,19 +97,7 @@ export declare class XtralabDiffWidget extends ReactWidget implements Git.Diff.I
104
97
  */
105
98
  get hasNotebookViewChanged(): ISignal<this, boolean>;
106
99
  /**
107
- * The current split/unified layout for textual file diffs.
108
- */
109
- get diffStyle(): DiffStyle;
110
- /**
111
- * Set the diff style and persist it.
112
- */
113
- setDiffStyle(style: DiffStyle): void;
114
- /**
115
- * A signal emitted when the diff style changes.
116
- */
117
- get diffStyleChanged(): ISignal<this, DiffStyle>;
118
- /**
119
- * Whether the Split/Unified toolbar selector currently applies.
100
+ * Whether the Split/Unified and line-wrap toolbar controls currently apply.
120
101
  */
121
102
  get fileDiffActive(): boolean;
122
103
  /**
@@ -159,12 +140,8 @@ export declare class XtralabDiffWidget extends ReactWidget implements Git.Diff.I
159
140
  private _context;
160
141
  private _reloadNonce;
161
142
  private _pendingRefresh;
162
- private _notebookViewMode;
163
- private _notebookViewModeChanged;
164
143
  private _hasNotebookView;
165
144
  private _hasNotebookViewChanged;
166
- private _diffStyle;
167
- private _diffStyleChanged;
168
145
  private _fileDiffActive;
169
146
  private _fileDiffActiveChanged;
170
147
  private _emptied;