jupyterlab_advanced_image_viewer_extension 1.0.2 → 1.0.4
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/lib/controller.d.ts +1 -1
- package/lib/controller.js +28 -10
- package/lib/index.js +40 -1
- package/package.json +3 -2
- package/src/__tests__/controller.spec.ts +84 -0
- package/src/controller.ts +29 -11
- package/src/index.ts +47 -1
- package/style/base.css +42 -0
package/lib/controller.d.ts
CHANGED
package/lib/controller.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// ViewerController adds zoom + pan ON TOP of JupyterLab's stock image viewer.
|
|
2
|
-
// The stock viewer
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
2
|
+
// The stock viewer OWNS the <img> transform - it writes rotate / flip / its own
|
|
3
|
+
// scale there for its keybindings (], [, h, v, 0). If we also wrote the <img>
|
|
4
|
+
// transform we would clobber that (and it would clobber ours). Instead we wrap
|
|
5
|
+
// the <img> in a pan layer and apply our translate+scale to the LAYER; the two
|
|
6
|
+
// transforms then compose. The stock CSS (object-fit: contain, max-width/height:
|
|
7
|
+
// 100%) still fits the image inside the layer, so the identity layer transform
|
|
8
|
+
// equals the correct fit-to-screen view, and stock rotate/flip keeps working.
|
|
8
9
|
const MIN_SCALE = 0.05;
|
|
9
10
|
const MAX_SCALE = 40;
|
|
10
11
|
export class ViewerController {
|
|
@@ -48,10 +49,20 @@ export class ViewerController {
|
|
|
48
49
|
window.removeEventListener('mouseup', this.onUp);
|
|
49
50
|
};
|
|
50
51
|
this.host = host;
|
|
51
|
-
this.img = img;
|
|
52
52
|
this.zoomStep = zoomStep;
|
|
53
53
|
this.host.style.overflow = 'hidden';
|
|
54
|
-
|
|
54
|
+
// Wrap the image so our zoom/pan compose with the stock rotate/flip
|
|
55
|
+
// transform that the viewer writes onto the <img> element itself. The
|
|
56
|
+
// layer fills the host (which is a positioned widget), so the stock
|
|
57
|
+
// object-fit/max-width CSS fits the image exactly as before.
|
|
58
|
+
const layer = document.createElement('div');
|
|
59
|
+
layer.className = 'jp-AdvancedImageViewer-panlayer';
|
|
60
|
+
const parent = img.parentNode;
|
|
61
|
+
if (parent) {
|
|
62
|
+
parent.insertBefore(layer, img);
|
|
63
|
+
}
|
|
64
|
+
layer.appendChild(img);
|
|
65
|
+
this.layer = layer;
|
|
55
66
|
this.host.addEventListener('wheel', this.onWheel, { passive: false });
|
|
56
67
|
this.host.addEventListener('mousedown', this.onDown);
|
|
57
68
|
this.ro = new ResizeObserver(() => this.onResize());
|
|
@@ -119,8 +130,8 @@ export class ViewerController {
|
|
|
119
130
|
this.host.style.cursor = this.canPan() ? 'grab' : 'default';
|
|
120
131
|
}
|
|
121
132
|
apply() {
|
|
122
|
-
this.
|
|
123
|
-
this.
|
|
133
|
+
this.layer.style.transformOrigin = 'center center';
|
|
134
|
+
this.layer.style.transform = `translate(${this.tx}px, ${this.ty}px) scale(${this.s})`;
|
|
124
135
|
this.updateCursor();
|
|
125
136
|
}
|
|
126
137
|
dispose() {
|
|
@@ -133,5 +144,12 @@ export class ViewerController {
|
|
|
133
144
|
this.host.removeEventListener('mousedown', this.onDown);
|
|
134
145
|
window.removeEventListener('mousemove', this.onMove);
|
|
135
146
|
window.removeEventListener('mouseup', this.onUp);
|
|
147
|
+
// Unwrap: restore the <img> as a direct child and drop the layer.
|
|
148
|
+
const img = this.layer.querySelector('img');
|
|
149
|
+
const parent = this.layer.parentNode;
|
|
150
|
+
if (img && parent) {
|
|
151
|
+
parent.insertBefore(img, this.layer);
|
|
152
|
+
}
|
|
153
|
+
this.layer.remove();
|
|
136
154
|
}
|
|
137
155
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { ToolbarButton } from '@jupyterlab/apputils';
|
|
1
|
+
import { Dialog, showDialog, Toolbar, ToolbarButton } from '@jupyterlab/apputils';
|
|
2
2
|
import { PathExt } from '@jupyterlab/coreutils';
|
|
3
3
|
import { IImageTracker } from '@jupyterlab/imageviewer';
|
|
4
4
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
5
|
+
import { Widget } from '@lumino/widgets';
|
|
5
6
|
import { ViewerController } from './controller';
|
|
6
7
|
const PLUGIN_ID = 'jupyterlab_advanced_image_viewer_extension:plugin';
|
|
7
8
|
const CommandIDs = {
|
|
@@ -43,6 +44,32 @@ const plugin = {
|
|
|
43
44
|
}
|
|
44
45
|
return (_a = controllers.get(widget.content)) !== null && _a !== void 0 ? _a : null;
|
|
45
46
|
};
|
|
47
|
+
const showHelp = () => {
|
|
48
|
+
const body = new Widget();
|
|
49
|
+
body.addClass('jp-AdvancedImageViewer-help');
|
|
50
|
+
body.node.innerHTML = [
|
|
51
|
+
'<p>Interactive viewing layered on the standard image viewer.</p>',
|
|
52
|
+
'<p class="jp-AdvancedImageViewer-help-section">This extension</p>',
|
|
53
|
+
'<ul>',
|
|
54
|
+
'<li><b>Wheel up / down</b> - zoom in / out at the cursor</li>',
|
|
55
|
+
'<li><b>Click and drag</b> - pan a zoomed-in image</li>',
|
|
56
|
+
'<li><b>+ / - / Fit</b> - zoom in, zoom out, reset to fit</li>',
|
|
57
|
+
'<li><b>Left / Right arrows</b> - previous / next image in the folder</li>',
|
|
58
|
+
'</ul>',
|
|
59
|
+
'<p class="jp-AdvancedImageViewer-help-section">Standard viewer keys (compose with the above)</p>',
|
|
60
|
+
'<ul>',
|
|
61
|
+
'<li><b>] / [</b> - rotate clockwise / counter-clockwise</li>',
|
|
62
|
+
'<li><b>H / V</b> - flip horizontal / vertical</li>',
|
|
63
|
+
'<li><b>I</b> - invert colours</li>',
|
|
64
|
+
'<li><b>= / - / 0</b> - zoom in / out / reset</li>',
|
|
65
|
+
'</ul>'
|
|
66
|
+
].join('');
|
|
67
|
+
void showDialog({
|
|
68
|
+
title: 'Advanced Image Viewer',
|
|
69
|
+
body,
|
|
70
|
+
buttons: [Dialog.okButton({ label: 'Close' })]
|
|
71
|
+
});
|
|
72
|
+
};
|
|
46
73
|
const attach = (widget) => {
|
|
47
74
|
const viewer = widget.content;
|
|
48
75
|
if (controllers.has(viewer)) {
|
|
@@ -74,6 +101,18 @@ const plugin = {
|
|
|
74
101
|
tooltip: 'Reset to fit',
|
|
75
102
|
onClick: () => app.commands.execute(CommandIDs.resetFit)
|
|
76
103
|
}));
|
|
104
|
+
widget.toolbar.addItem('advanced-spacer', Toolbar.createSpacerItem());
|
|
105
|
+
const helpLink = new Widget({ node: document.createElement('a') });
|
|
106
|
+
const helpAnchor = helpLink.node;
|
|
107
|
+
helpAnchor.textContent = 'help';
|
|
108
|
+
helpAnchor.href = '#';
|
|
109
|
+
helpAnchor.title = 'Keybindings and help';
|
|
110
|
+
helpAnchor.className = 'jp-AdvancedImageViewer-help-link';
|
|
111
|
+
helpAnchor.addEventListener('click', event => {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
showHelp();
|
|
114
|
+
});
|
|
115
|
+
widget.toolbar.addItem('advanced-help', helpLink);
|
|
77
116
|
};
|
|
78
117
|
tracker.forEach(widget => attach(widget));
|
|
79
118
|
tracker.widgetAdded.connect((_, widget) => attach(widget));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupyterlab_advanced_image_viewer_extension",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "JupyterLab extension for advanced image viewing: fit-to-screen, cursor-anchored wheel zoom, drag-to-pan, and Left/Right arrow keys to move to the previous and next image in a folder",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
"@jupyterlab/docregistry": "^4.0.0",
|
|
63
63
|
"@jupyterlab/imageviewer": "^4.0.0",
|
|
64
64
|
"@jupyterlab/settingregistry": "^4.0.0",
|
|
65
|
-
"@lumino/disposable": "^2.0.0"
|
|
65
|
+
"@lumino/disposable": "^2.0.0",
|
|
66
|
+
"@lumino/widgets": "^2.0.0"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
69
|
"@jupyterlab/builder": "^4.0.0",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ViewerController } from '../controller';
|
|
2
|
+
|
|
3
|
+
// jsdom has no ResizeObserver; provide a no-op stub.
|
|
4
|
+
class MockResizeObserver {
|
|
5
|
+
observe(): void {
|
|
6
|
+
/* no-op */
|
|
7
|
+
}
|
|
8
|
+
unobserve(): void {
|
|
9
|
+
/* no-op */
|
|
10
|
+
}
|
|
11
|
+
disconnect(): void {
|
|
12
|
+
/* no-op */
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('ViewerController', () => {
|
|
17
|
+
let host: HTMLElement;
|
|
18
|
+
let img: HTMLImageElement;
|
|
19
|
+
|
|
20
|
+
beforeAll(() => {
|
|
21
|
+
(window as any).ResizeObserver = MockResizeObserver;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
host = document.createElement('div');
|
|
26
|
+
img = document.createElement('img');
|
|
27
|
+
host.appendChild(img);
|
|
28
|
+
document.body.appendChild(host);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
host.remove();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('wraps the image in a pan layer so the stock viewer keeps the img transform', () => {
|
|
36
|
+
const c = new ViewerController(host, img, 0.1);
|
|
37
|
+
const layer = host.querySelector('.jp-AdvancedImageViewer-panlayer');
|
|
38
|
+
expect(layer).not.toBeNull();
|
|
39
|
+
expect(img.parentElement).toBe(layer);
|
|
40
|
+
c.dispose();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('applies the transform to the layer, never to the image', () => {
|
|
44
|
+
const c = new ViewerController(host, img, 0.1);
|
|
45
|
+
const layer = host.querySelector(
|
|
46
|
+
'.jp-AdvancedImageViewer-panlayer'
|
|
47
|
+
) as HTMLElement;
|
|
48
|
+
expect(layer.style.transform).toBe('translate(0px, 0px) scale(1)');
|
|
49
|
+
expect(img.style.transform).toBe('');
|
|
50
|
+
c.dispose();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('zooms the layer in, allows zoom-out below fit, and resets to identity', () => {
|
|
54
|
+
const c = new ViewerController(host, img, 0.1);
|
|
55
|
+
const layer = host.querySelector(
|
|
56
|
+
'.jp-AdvancedImageViewer-panlayer'
|
|
57
|
+
) as HTMLElement;
|
|
58
|
+
c.zoomIn();
|
|
59
|
+
expect(layer.style.transform).toContain('scale(1.1)');
|
|
60
|
+
c.reset();
|
|
61
|
+
expect(layer.style.transform).toBe('translate(0px, 0px) scale(1)');
|
|
62
|
+
c.zoomOut();
|
|
63
|
+
const scale = parseFloat(
|
|
64
|
+
/scale\(([0-9.]+)\)/.exec(layer.style.transform)![1]
|
|
65
|
+
);
|
|
66
|
+
expect(scale).toBeLessThan(1);
|
|
67
|
+
c.dispose();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('never writes the image transform even after zooming (no clobber of rotate/flip)', () => {
|
|
71
|
+
const c = new ViewerController(host, img, 0.1);
|
|
72
|
+
c.zoomIn();
|
|
73
|
+
c.zoomIn();
|
|
74
|
+
expect(img.style.transform).toBe('');
|
|
75
|
+
c.dispose();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('unwraps the image on dispose', () => {
|
|
79
|
+
const c = new ViewerController(host, img, 0.1);
|
|
80
|
+
c.dispose();
|
|
81
|
+
expect(host.querySelector('.jp-AdvancedImageViewer-panlayer')).toBeNull();
|
|
82
|
+
expect(img.parentElement).toBe(host);
|
|
83
|
+
});
|
|
84
|
+
});
|
package/src/controller.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
// ViewerController adds zoom + pan ON TOP of JupyterLab's stock image viewer.
|
|
2
|
-
// The stock viewer
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
2
|
+
// The stock viewer OWNS the <img> transform - it writes rotate / flip / its own
|
|
3
|
+
// scale there for its keybindings (], [, h, v, 0). If we also wrote the <img>
|
|
4
|
+
// transform we would clobber that (and it would clobber ours). Instead we wrap
|
|
5
|
+
// the <img> in a pan layer and apply our translate+scale to the LAYER; the two
|
|
6
|
+
// transforms then compose. The stock CSS (object-fit: contain, max-width/height:
|
|
7
|
+
// 100%) still fits the image inside the layer, so the identity layer transform
|
|
8
|
+
// equals the correct fit-to-screen view, and stock rotate/flip keeps working.
|
|
8
9
|
|
|
9
10
|
const MIN_SCALE = 0.05;
|
|
10
11
|
const MAX_SCALE = 40;
|
|
11
12
|
|
|
12
13
|
export class ViewerController {
|
|
13
14
|
private host: HTMLElement;
|
|
14
|
-
private
|
|
15
|
+
private layer: HTMLElement;
|
|
15
16
|
private s = 1;
|
|
16
17
|
private tx = 0;
|
|
17
18
|
private ty = 0;
|
|
@@ -23,10 +24,20 @@ export class ViewerController {
|
|
|
23
24
|
|
|
24
25
|
constructor(host: HTMLElement, img: HTMLImageElement, zoomStep: number) {
|
|
25
26
|
this.host = host;
|
|
26
|
-
this.img = img;
|
|
27
27
|
this.zoomStep = zoomStep;
|
|
28
28
|
this.host.style.overflow = 'hidden';
|
|
29
|
-
|
|
29
|
+
// Wrap the image so our zoom/pan compose with the stock rotate/flip
|
|
30
|
+
// transform that the viewer writes onto the <img> element itself. The
|
|
31
|
+
// layer fills the host (which is a positioned widget), so the stock
|
|
32
|
+
// object-fit/max-width CSS fits the image exactly as before.
|
|
33
|
+
const layer = document.createElement('div');
|
|
34
|
+
layer.className = 'jp-AdvancedImageViewer-panlayer';
|
|
35
|
+
const parent = img.parentNode;
|
|
36
|
+
if (parent) {
|
|
37
|
+
parent.insertBefore(layer, img);
|
|
38
|
+
}
|
|
39
|
+
layer.appendChild(img);
|
|
40
|
+
this.layer = layer;
|
|
30
41
|
this.host.addEventListener('wheel', this.onWheel, { passive: false });
|
|
31
42
|
this.host.addEventListener('mousedown', this.onDown);
|
|
32
43
|
this.ro = new ResizeObserver(() => this.onResize());
|
|
@@ -142,8 +153,8 @@ export class ViewerController {
|
|
|
142
153
|
}
|
|
143
154
|
|
|
144
155
|
private apply(): void {
|
|
145
|
-
this.
|
|
146
|
-
this.
|
|
156
|
+
this.layer.style.transformOrigin = 'center center';
|
|
157
|
+
this.layer.style.transform = `translate(${this.tx}px, ${this.ty}px) scale(${this.s})`;
|
|
147
158
|
this.updateCursor();
|
|
148
159
|
}
|
|
149
160
|
|
|
@@ -157,5 +168,12 @@ export class ViewerController {
|
|
|
157
168
|
this.host.removeEventListener('mousedown', this.onDown);
|
|
158
169
|
window.removeEventListener('mousemove', this.onMove);
|
|
159
170
|
window.removeEventListener('mouseup', this.onUp);
|
|
171
|
+
// Unwrap: restore the <img> as a direct child and drop the layer.
|
|
172
|
+
const img = this.layer.querySelector('img');
|
|
173
|
+
const parent = this.layer.parentNode;
|
|
174
|
+
if (img && parent) {
|
|
175
|
+
parent.insertBefore(img, this.layer);
|
|
176
|
+
}
|
|
177
|
+
this.layer.remove();
|
|
160
178
|
}
|
|
161
179
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,12 @@ import {
|
|
|
3
3
|
JupyterFrontEndPlugin
|
|
4
4
|
} from '@jupyterlab/application';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
showDialog,
|
|
9
|
+
Toolbar,
|
|
10
|
+
ToolbarButton
|
|
11
|
+
} from '@jupyterlab/apputils';
|
|
7
12
|
|
|
8
13
|
import { PathExt } from '@jupyterlab/coreutils';
|
|
9
14
|
|
|
@@ -15,6 +20,8 @@ import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
|
15
20
|
|
|
16
21
|
import { IDisposable } from '@lumino/disposable';
|
|
17
22
|
|
|
23
|
+
import { Widget } from '@lumino/widgets';
|
|
24
|
+
|
|
18
25
|
import { ViewerController } from './controller';
|
|
19
26
|
|
|
20
27
|
const PLUGIN_ID = 'jupyterlab_advanced_image_viewer_extension:plugin';
|
|
@@ -74,6 +81,33 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
74
81
|
return controllers.get(widget.content) ?? null;
|
|
75
82
|
};
|
|
76
83
|
|
|
84
|
+
const showHelp = (): void => {
|
|
85
|
+
const body = new Widget();
|
|
86
|
+
body.addClass('jp-AdvancedImageViewer-help');
|
|
87
|
+
body.node.innerHTML = [
|
|
88
|
+
'<p>Interactive viewing layered on the standard image viewer.</p>',
|
|
89
|
+
'<p class="jp-AdvancedImageViewer-help-section">This extension</p>',
|
|
90
|
+
'<ul>',
|
|
91
|
+
'<li><b>Wheel up / down</b> - zoom in / out at the cursor</li>',
|
|
92
|
+
'<li><b>Click and drag</b> - pan a zoomed-in image</li>',
|
|
93
|
+
'<li><b>+ / - / Fit</b> - zoom in, zoom out, reset to fit</li>',
|
|
94
|
+
'<li><b>Left / Right arrows</b> - previous / next image in the folder</li>',
|
|
95
|
+
'</ul>',
|
|
96
|
+
'<p class="jp-AdvancedImageViewer-help-section">Standard viewer keys (compose with the above)</p>',
|
|
97
|
+
'<ul>',
|
|
98
|
+
'<li><b>] / [</b> - rotate clockwise / counter-clockwise</li>',
|
|
99
|
+
'<li><b>H / V</b> - flip horizontal / vertical</li>',
|
|
100
|
+
'<li><b>I</b> - invert colours</li>',
|
|
101
|
+
'<li><b>= / - / 0</b> - zoom in / out / reset</li>',
|
|
102
|
+
'</ul>'
|
|
103
|
+
].join('');
|
|
104
|
+
void showDialog({
|
|
105
|
+
title: 'Advanced Image Viewer',
|
|
106
|
+
body,
|
|
107
|
+
buttons: [Dialog.okButton({ label: 'Close' })]
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
77
111
|
const attach = (widget: IDocumentWidget<ImageViewer>): void => {
|
|
78
112
|
const viewer = widget.content;
|
|
79
113
|
if (controllers.has(viewer)) {
|
|
@@ -119,6 +153,18 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
119
153
|
onClick: () => app.commands.execute(CommandIDs.resetFit)
|
|
120
154
|
})
|
|
121
155
|
);
|
|
156
|
+
widget.toolbar.addItem('advanced-spacer', Toolbar.createSpacerItem());
|
|
157
|
+
const helpLink = new Widget({ node: document.createElement('a') });
|
|
158
|
+
const helpAnchor = helpLink.node as HTMLAnchorElement;
|
|
159
|
+
helpAnchor.textContent = 'help';
|
|
160
|
+
helpAnchor.href = '#';
|
|
161
|
+
helpAnchor.title = 'Keybindings and help';
|
|
162
|
+
helpAnchor.className = 'jp-AdvancedImageViewer-help-link';
|
|
163
|
+
helpAnchor.addEventListener('click', event => {
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
showHelp();
|
|
166
|
+
});
|
|
167
|
+
widget.toolbar.addItem('advanced-help', helpLink);
|
|
122
168
|
};
|
|
123
169
|
|
|
124
170
|
tracker.forEach(widget => attach(widget));
|
package/style/base.css
CHANGED
|
@@ -3,3 +3,45 @@
|
|
|
3
3
|
|
|
4
4
|
https://jupyterlab.readthedocs.io/en/stable/developer/css.html
|
|
5
5
|
*/
|
|
6
|
+
|
|
7
|
+
/* Pan/zoom layer wrapping the <img> so our transform composes with the stock
|
|
8
|
+
viewer's rotate/flip transform on the image itself. Fills the host. */
|
|
9
|
+
.jp-AdvancedImageViewer-panlayer {
|
|
10
|
+
position: absolute;
|
|
11
|
+
inset: 0;
|
|
12
|
+
transform-origin: center center;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Help shown as an accent-coloured link at the right end of the toolbar.
|
|
16
|
+
The selector is scoped under .jp-Toolbar to beat JupyterLab's toolbar `a`
|
|
17
|
+
rule (otherwise the link renders in the default muted toolbar colour). */
|
|
18
|
+
.jp-Toolbar a.jp-AdvancedImageViewer-help-link {
|
|
19
|
+
align-self: center;
|
|
20
|
+
margin: 0 8px;
|
|
21
|
+
color: var(--jp-brand-color1);
|
|
22
|
+
font-size: var(--jp-ui-font-size1);
|
|
23
|
+
text-decoration: underline;
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.jp-Toolbar a.jp-AdvancedImageViewer-help-link:hover {
|
|
28
|
+
color: var(--jp-brand-color0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.jp-AdvancedImageViewer-help {
|
|
32
|
+
max-width: 380px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.jp-AdvancedImageViewer-help-section {
|
|
36
|
+
margin: 12px 0 0;
|
|
37
|
+
font-weight: 600;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.jp-AdvancedImageViewer-help ul {
|
|
41
|
+
margin: 8px 0 0;
|
|
42
|
+
padding-left: 18px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.jp-AdvancedImageViewer-help li {
|
|
46
|
+
margin: 2px 0;
|
|
47
|
+
}
|