jupyterlab_commands_extension 1.0.18 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/lib/index.js +38 -0
- package/lib/widget.js +2 -1
- package/package.json +1 -1
- package/schema/plugin.json +2 -1
- package/src/__tests__/file-download.spec.ts +121 -0
- package/src/index.ts +41 -0
- package/src/widget.ts +2 -1
- package/style/base.css +127 -18
package/README.md
CHANGED
|
@@ -55,16 +55,26 @@ JUPYTERLAB_COMMANDS_TOKEN=... jupyterlab-commands --url https://remote-host:8888
|
|
|
55
55
|
jupyterlab-commands activity
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
`file:download` starts a browser download of a file the contents API serves. The `path` argument names the file relative to the server root; the file must exist, and folders are refused. The download starts in the browser tab that runs the relay, so the file lands on the user computer.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
jupyterlab-commands execute file:download --args '{"path": "data/report.csv"}'
|
|
62
|
+
```
|
|
63
|
+
|
|
58
64
|
Exit codes: 0 success, 2 discovery or usage error, 3 no browser client, 4 command not allowed, 5 execution error.
|
|
59
65
|
|
|
60
66
|
The browser tab is the executor: the command runs in the lab, not in the terminal.
|
|
61
67
|
|
|
68
|
+
Every execute request reaches the JupyterLab server log: the outcome, the command ID, the arguments as compact JSON truncated to 500 characters, the elapsed milliseconds and the caller's remote IP, at INFO on success and WARNING on refusal or failure.
|
|
69
|
+
|
|
62
70
|
## Allowlist
|
|
63
71
|
|
|
64
|
-
The `allowedCommands` setting holds the command IDs the CLI may run. The defaults are `docmanager:open`, `help:open`, `apputils:notify`, `notebook:create-new`, `console:create`, `terminal:create-new`, `launcher:create
|
|
72
|
+
The `allowedCommands` setting holds the command IDs the CLI may run. The defaults are `docmanager:open`, `help:open`, `apputils:notify`, `notebook:create-new`, `console:create`, `terminal:create-new`, `launcher:create`, `jupyterlab-commands-reference:open` and `file:download`.
|
|
65
73
|
|
|
66
74
|
The **Add to command executor** button in the Command Executor column of the reference tab appends that command ID to the setting, and the button then shows **Added**. The setting can also be edited directly in **Settings > Settings Editor > Commands Executor**, whose plugin ID is `jupyterlab_commands_extension:plugin`.
|
|
67
75
|
|
|
76
|
+
A user with a saved `allowedCommands` list keeps it; the extension does not rewrite the list, so add `file:download` with the **Add to command executor** button.
|
|
77
|
+
|
|
68
78
|
The reference tab opens from the command palette as **Show Commands Reference** under Help, and carries the text editor icon.
|
|
69
79
|
|
|
70
80
|
## Agent skill
|
package/lib/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { CommandsReferenceWidget } from './widget';
|
|
|
9
9
|
var CommandIDs;
|
|
10
10
|
(function (CommandIDs) {
|
|
11
11
|
CommandIDs.open = 'jupyterlab-commands-reference:open';
|
|
12
|
+
CommandIDs.download = 'file:download';
|
|
12
13
|
})(CommandIDs || (CommandIDs = {}));
|
|
13
14
|
/**
|
|
14
15
|
* The id of this plugin. It is also the id of its settings schema.
|
|
@@ -70,6 +71,43 @@ const plugin = {
|
|
|
70
71
|
command: CommandIDs.open,
|
|
71
72
|
category: 'Help'
|
|
72
73
|
});
|
|
74
|
+
// Download a file the contents API serves to the computer that runs
|
|
75
|
+
// the browser tab.
|
|
76
|
+
app.commands.addCommand(CommandIDs.download, {
|
|
77
|
+
label: 'Download File',
|
|
78
|
+
caption: 'Download a file from the server to the local computer',
|
|
79
|
+
describedBy: () => ({
|
|
80
|
+
args: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
path: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'the file path relative to the server root'
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
required: ['path']
|
|
89
|
+
}
|
|
90
|
+
}),
|
|
91
|
+
execute: async (args) => {
|
|
92
|
+
const path = typeof args.path === 'string' ? args.path : '';
|
|
93
|
+
if (!path) {
|
|
94
|
+
throw new Error('file:download requires a non-empty path argument');
|
|
95
|
+
}
|
|
96
|
+
const model = await app.serviceManager.contents.get(path, {
|
|
97
|
+
content: false
|
|
98
|
+
});
|
|
99
|
+
if (model.type === 'directory') {
|
|
100
|
+
throw new Error(`file:download refuses a directory: ${path}`);
|
|
101
|
+
}
|
|
102
|
+
const url = await app.serviceManager.contents.getDownloadUrl(model.path);
|
|
103
|
+
const element = document.createElement('a');
|
|
104
|
+
element.href = url;
|
|
105
|
+
element.download = '';
|
|
106
|
+
document.body.appendChild(element);
|
|
107
|
+
element.click();
|
|
108
|
+
document.body.removeChild(element);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
73
111
|
if (settings) {
|
|
74
112
|
relay = new RelayClient({
|
|
75
113
|
app,
|
package/lib/widget.js
CHANGED
|
@@ -47,7 +47,8 @@ export class CommandsReferenceWidget extends Widget {
|
|
|
47
47
|
this._searchInput = document.createElement('input');
|
|
48
48
|
this._searchInput.type = 'text';
|
|
49
49
|
this._searchInput.placeholder = 'Filter commands...';
|
|
50
|
-
this._searchInput.className =
|
|
50
|
+
this._searchInput.className =
|
|
51
|
+
'jp-CommandsReferenceWidget-search jp-mod-styled';
|
|
51
52
|
this._searchInput.addEventListener('input', () => {
|
|
52
53
|
this._filterCommands();
|
|
53
54
|
});
|
package/package.json
CHANGED
package/schema/plugin.json
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { JupyterFrontEnd } from '@jupyterlab/application';
|
|
2
|
+
import type { ICommandPalette } from '@jupyterlab/apputils';
|
|
3
|
+
import type { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
4
|
+
import { CommandRegistry } from '@lumino/commands';
|
|
5
|
+
|
|
6
|
+
// The tests never run the reference-tab command, so mock the UI modules it
|
|
7
|
+
// reaches for: the real ones pull an untranspiled ESM chain.
|
|
8
|
+
jest.mock('@jupyterlab/apputils', () => ({
|
|
9
|
+
MainAreaWidget: jest.fn()
|
|
10
|
+
}));
|
|
11
|
+
jest.mock('@jupyterlab/ui-components', () => ({
|
|
12
|
+
textEditorIcon: null
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
import plugin from '../index';
|
|
16
|
+
|
|
17
|
+
const COMMAND_ID = 'file:download';
|
|
18
|
+
|
|
19
|
+
interface IFakeApp {
|
|
20
|
+
app: JupyterFrontEnd;
|
|
21
|
+
get: jest.Mock;
|
|
22
|
+
getDownloadUrl: jest.Mock;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Activate the plugin with a registry whose settings load fails, so the relay
|
|
27
|
+
* never starts, and return the contents mocks the command executes against.
|
|
28
|
+
*/
|
|
29
|
+
async function activatePlugin(): Promise<IFakeApp> {
|
|
30
|
+
const commands = new CommandRegistry();
|
|
31
|
+
const get = jest.fn();
|
|
32
|
+
const getDownloadUrl = jest.fn();
|
|
33
|
+
const app = {
|
|
34
|
+
commands,
|
|
35
|
+
serviceManager: { contents: { get, getDownloadUrl } }
|
|
36
|
+
} as unknown as JupyterFrontEnd;
|
|
37
|
+
const palette = { addItem: jest.fn() } as unknown as ICommandPalette;
|
|
38
|
+
const registry = {
|
|
39
|
+
load: jest.fn().mockRejectedValue(new Error('no settings in this test'))
|
|
40
|
+
} as unknown as ISettingRegistry;
|
|
41
|
+
await plugin.activate(app, palette, registry);
|
|
42
|
+
return { app, get, getDownloadUrl };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function spyOnAnchorClick(): jest.Mock {
|
|
46
|
+
const clicked = jest.fn();
|
|
47
|
+
jest.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(function (
|
|
48
|
+
this: HTMLAnchorElement
|
|
49
|
+
) {
|
|
50
|
+
clicked({ element: this, parent: this.parentNode });
|
|
51
|
+
});
|
|
52
|
+
return clicked;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('file:download', () => {
|
|
56
|
+
it('registers the command with the describedBy path argument', async () => {
|
|
57
|
+
const { app } = await activatePlugin();
|
|
58
|
+
|
|
59
|
+
expect(app.commands.hasCommand(COMMAND_ID)).toBe(true);
|
|
60
|
+
expect(app.commands.label(COMMAND_ID)).toBe('Download File');
|
|
61
|
+
const description = await app.commands.describedBy(COMMAND_ID);
|
|
62
|
+
const args = description.args as Record<string, any>;
|
|
63
|
+
expect(args.type).toBe('object');
|
|
64
|
+
expect(args.properties.path.type).toBe('string');
|
|
65
|
+
expect(args.properties.path.description).toBe(
|
|
66
|
+
'the file path relative to the server root'
|
|
67
|
+
);
|
|
68
|
+
expect(args.required).toEqual(['path']);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('rejects when path is missing or not a non-empty string', async () => {
|
|
72
|
+
const { app, get } = await activatePlugin();
|
|
73
|
+
|
|
74
|
+
await expect(app.commands.execute(COMMAND_ID, {})).rejects.toThrow(
|
|
75
|
+
'non-empty path'
|
|
76
|
+
);
|
|
77
|
+
await expect(
|
|
78
|
+
app.commands.execute(COMMAND_ID, { path: '' })
|
|
79
|
+
).rejects.toThrow('non-empty path');
|
|
80
|
+
await expect(app.commands.execute(COMMAND_ID, { path: 3 })).rejects.toThrow(
|
|
81
|
+
'non-empty path'
|
|
82
|
+
);
|
|
83
|
+
expect(get).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('rejects a directory and clicks nothing', async () => {
|
|
87
|
+
const { app, get, getDownloadUrl } = await activatePlugin();
|
|
88
|
+
get.mockResolvedValue({ type: 'directory', path: 'data' });
|
|
89
|
+
const clicked = spyOnAnchorClick();
|
|
90
|
+
|
|
91
|
+
await expect(
|
|
92
|
+
app.commands.execute(COMMAND_ID, { path: 'data' })
|
|
93
|
+
).rejects.toThrow(/directory: data/);
|
|
94
|
+
expect(get).toHaveBeenCalledWith('data', { content: false });
|
|
95
|
+
expect(getDownloadUrl).not.toHaveBeenCalled();
|
|
96
|
+
expect(clicked).not.toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('downloads a file through a temporary anchor', async () => {
|
|
100
|
+
const { app, get, getDownloadUrl } = await activatePlugin();
|
|
101
|
+
get.mockResolvedValue({ type: 'file', path: 'data/report.csv' });
|
|
102
|
+
const url = 'http://host/files/data/report.csv';
|
|
103
|
+
getDownloadUrl.mockResolvedValue(url);
|
|
104
|
+
const clicked = spyOnAnchorClick();
|
|
105
|
+
|
|
106
|
+
await expect(
|
|
107
|
+
app.commands.execute(COMMAND_ID, { path: 'data/report.csv' })
|
|
108
|
+
).resolves.toBeUndefined();
|
|
109
|
+
expect(get).toHaveBeenCalledWith('data/report.csv', { content: false });
|
|
110
|
+
expect(getDownloadUrl).toHaveBeenCalledWith('data/report.csv');
|
|
111
|
+
expect(clicked).toHaveBeenCalledTimes(1);
|
|
112
|
+
const { element: anchor, parent } = clicked.mock.calls[0][0] as {
|
|
113
|
+
element: HTMLAnchorElement;
|
|
114
|
+
parent: Node | null;
|
|
115
|
+
};
|
|
116
|
+
expect(anchor.href).toBe(url);
|
|
117
|
+
expect(anchor.download).toBe('');
|
|
118
|
+
expect(parent).toBe(document.body);
|
|
119
|
+
expect(document.querySelector('a')).toBeNull();
|
|
120
|
+
});
|
|
121
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { CommandsReferenceWidget } from './widget';
|
|
|
17
17
|
*/
|
|
18
18
|
namespace CommandIDs {
|
|
19
19
|
export const open = 'jupyterlab-commands-reference:open';
|
|
20
|
+
export const download = 'file:download';
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -103,6 +104,46 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
103
104
|
category: 'Help'
|
|
104
105
|
});
|
|
105
106
|
|
|
107
|
+
// Download a file the contents API serves to the computer that runs
|
|
108
|
+
// the browser tab.
|
|
109
|
+
app.commands.addCommand(CommandIDs.download, {
|
|
110
|
+
label: 'Download File',
|
|
111
|
+
caption: 'Download a file from the server to the local computer',
|
|
112
|
+
describedBy: () => ({
|
|
113
|
+
args: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
path: {
|
|
117
|
+
type: 'string',
|
|
118
|
+
description: 'the file path relative to the server root'
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
required: ['path']
|
|
122
|
+
}
|
|
123
|
+
}),
|
|
124
|
+
execute: async args => {
|
|
125
|
+
const path = typeof args.path === 'string' ? args.path : '';
|
|
126
|
+
if (!path) {
|
|
127
|
+
throw new Error('file:download requires a non-empty path argument');
|
|
128
|
+
}
|
|
129
|
+
const model = await app.serviceManager.contents.get(path, {
|
|
130
|
+
content: false
|
|
131
|
+
});
|
|
132
|
+
if (model.type === 'directory') {
|
|
133
|
+
throw new Error(`file:download refuses a directory: ${path}`);
|
|
134
|
+
}
|
|
135
|
+
const url = await app.serviceManager.contents.getDownloadUrl(
|
|
136
|
+
model.path
|
|
137
|
+
);
|
|
138
|
+
const element = document.createElement('a');
|
|
139
|
+
element.href = url;
|
|
140
|
+
element.download = '';
|
|
141
|
+
document.body.appendChild(element);
|
|
142
|
+
element.click();
|
|
143
|
+
document.body.removeChild(element);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
106
147
|
if (settings) {
|
|
107
148
|
relay = new RelayClient({
|
|
108
149
|
app,
|
package/src/widget.ts
CHANGED
|
@@ -69,7 +69,8 @@ export class CommandsReferenceWidget extends Widget {
|
|
|
69
69
|
this._searchInput = document.createElement('input');
|
|
70
70
|
this._searchInput.type = 'text';
|
|
71
71
|
this._searchInput.placeholder = 'Filter commands...';
|
|
72
|
-
this._searchInput.className =
|
|
72
|
+
this._searchInput.className =
|
|
73
|
+
'jp-CommandsReferenceWidget-search jp-mod-styled';
|
|
73
74
|
this._searchInput.addEventListener('input', () => {
|
|
74
75
|
this._filterCommands();
|
|
75
76
|
});
|
package/style/base.css
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
flex-direction: column;
|
|
11
11
|
height: 100%;
|
|
12
12
|
overflow: hidden;
|
|
13
|
+
container-type: inline-size;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
/* Header with search and count */
|
|
@@ -23,23 +24,28 @@
|
|
|
23
24
|
flex-shrink: 0;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
/*
|
|
27
|
-
.jp-
|
|
27
|
+
/*
|
|
28
|
+
* Search input. jp-mod-styled supplies the same properties at a higher
|
|
29
|
+
* specificity, so each rule repeats the class after it.
|
|
30
|
+
*/
|
|
31
|
+
input.jp-mod-styled.jp-CommandsReferenceWidget-search {
|
|
28
32
|
flex: 1;
|
|
29
|
-
|
|
33
|
+
height: 24px;
|
|
34
|
+
padding: 0 9px;
|
|
30
35
|
border: 1px solid var(--jp-border-color1);
|
|
31
|
-
border-radius:
|
|
36
|
+
border-radius: 2px;
|
|
32
37
|
background: var(--jp-layout-color0);
|
|
33
38
|
color: var(--jp-ui-font-color1);
|
|
34
39
|
font-size: var(--jp-ui-font-size1);
|
|
35
40
|
outline: none;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
.jp-CommandsReferenceWidget-search:focus {
|
|
43
|
+
input.jp-mod-styled.jp-CommandsReferenceWidget-search:focus {
|
|
39
44
|
border-color: var(--jp-brand-color1);
|
|
45
|
+
box-shadow: none;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
.jp-CommandsReferenceWidget-search::placeholder {
|
|
48
|
+
input.jp-mod-styled.jp-CommandsReferenceWidget-search::placeholder {
|
|
43
49
|
color: var(--jp-ui-font-color3);
|
|
44
50
|
}
|
|
45
51
|
|
|
@@ -60,6 +66,7 @@
|
|
|
60
66
|
/* Commands table */
|
|
61
67
|
.jp-CommandsReferenceWidget-table {
|
|
62
68
|
width: 100%;
|
|
69
|
+
table-layout: fixed;
|
|
63
70
|
border-collapse: collapse;
|
|
64
71
|
font-size: var(--jp-ui-font-size1);
|
|
65
72
|
}
|
|
@@ -73,21 +80,29 @@
|
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
.jp-CommandsReferenceWidget-table th {
|
|
76
|
-
|
|
83
|
+
box-sizing: border-box;
|
|
84
|
+
padding: 6px 12px;
|
|
85
|
+
overflow: hidden;
|
|
77
86
|
text-align: left;
|
|
78
|
-
|
|
79
|
-
color: var(--jp-ui-font-color1);
|
|
80
|
-
border-bottom: 2px solid var(--jp-border-color1);
|
|
87
|
+
text-overflow: ellipsis;
|
|
81
88
|
white-space: nowrap;
|
|
89
|
+
font-size: 10px;
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
line-height: 1;
|
|
92
|
+
letter-spacing: 0.08em;
|
|
93
|
+
text-transform: uppercase;
|
|
94
|
+
color: var(--jp-ui-font-color3);
|
|
95
|
+
border-bottom: 1px solid var(--jp-border-color2);
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
/* Table cells */
|
|
85
99
|
.jp-CommandsReferenceWidget-table td {
|
|
100
|
+
box-sizing: border-box;
|
|
86
101
|
padding: 6px 12px;
|
|
87
102
|
border-bottom: 1px solid var(--jp-border-color2);
|
|
88
103
|
color: var(--jp-ui-font-color1);
|
|
89
104
|
vertical-align: top;
|
|
90
|
-
|
|
105
|
+
white-space: nowrap;
|
|
91
106
|
overflow: hidden;
|
|
92
107
|
text-overflow: ellipsis;
|
|
93
108
|
}
|
|
@@ -101,6 +116,43 @@
|
|
|
101
116
|
white-space: nowrap;
|
|
102
117
|
}
|
|
103
118
|
|
|
119
|
+
/* Command executor cell - the row action sits at the right edge */
|
|
120
|
+
.jp-CommandsReferenceWidget-executor {
|
|
121
|
+
text-align: right;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* In-row action button */
|
|
125
|
+
.jp-CommandsReferenceWidget-add {
|
|
126
|
+
display: inline-block;
|
|
127
|
+
box-sizing: border-box;
|
|
128
|
+
max-width: 100%;
|
|
129
|
+
height: 20px;
|
|
130
|
+
padding: 0 8px;
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
border: 1px solid transparent;
|
|
133
|
+
border-radius: 2px;
|
|
134
|
+
background: transparent;
|
|
135
|
+
color: var(--jp-ui-font-color2);
|
|
136
|
+
font-family: inherit;
|
|
137
|
+
font-size: var(--jp-ui-font-size0);
|
|
138
|
+
line-height: 18px;
|
|
139
|
+
text-overflow: ellipsis;
|
|
140
|
+
white-space: nowrap;
|
|
141
|
+
vertical-align: top;
|
|
142
|
+
cursor: pointer;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* Added state, and no settings to add to */
|
|
146
|
+
.jp-CommandsReferenceWidget-add:disabled {
|
|
147
|
+
opacity: 0.45;
|
|
148
|
+
cursor: default;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.jp-CommandsReferenceWidget-add:enabled:hover {
|
|
152
|
+
background: var(--jp-layout-color2);
|
|
153
|
+
color: var(--jp-ui-font-color1);
|
|
154
|
+
}
|
|
155
|
+
|
|
104
156
|
/* Alternating row colors */
|
|
105
157
|
.jp-CommandsReferenceWidget-table tbody tr:nth-child(even) {
|
|
106
158
|
background: var(--jp-layout-color1);
|
|
@@ -115,27 +167,84 @@
|
|
|
115
167
|
background: var(--jp-layout-color2);
|
|
116
168
|
}
|
|
117
169
|
|
|
118
|
-
/* Column widths */
|
|
170
|
+
/* Column widths - five shares summing to 100% under the fixed layout */
|
|
119
171
|
.jp-CommandsReferenceWidget-table th:nth-child(1),
|
|
120
172
|
.jp-CommandsReferenceWidget-table td:nth-child(1) {
|
|
121
|
-
width:
|
|
122
|
-
min-width: 200px;
|
|
173
|
+
width: 25%;
|
|
123
174
|
}
|
|
124
175
|
|
|
125
176
|
.jp-CommandsReferenceWidget-table th:nth-child(2),
|
|
126
177
|
.jp-CommandsReferenceWidget-table td:nth-child(2) {
|
|
127
|
-
width:
|
|
178
|
+
width: 17%;
|
|
128
179
|
}
|
|
129
180
|
|
|
130
181
|
.jp-CommandsReferenceWidget-table th:nth-child(3),
|
|
131
182
|
.jp-CommandsReferenceWidget-table td:nth-child(3) {
|
|
132
|
-
width:
|
|
183
|
+
width: 26%;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.jp-CommandsReferenceWidget-table th:nth-child(4) {
|
|
187
|
+
width: 17%;
|
|
133
188
|
}
|
|
134
189
|
|
|
135
|
-
.jp-CommandsReferenceWidget-table th:nth-child(4),
|
|
136
190
|
.jp-CommandsReferenceWidget-table td:nth-child(4) {
|
|
137
|
-
width:
|
|
191
|
+
width: 17%;
|
|
138
192
|
font-family: var(--jp-code-font-family);
|
|
139
193
|
font-size: var(--jp-code-font-size);
|
|
140
194
|
color: var(--jp-ui-font-color2);
|
|
141
195
|
}
|
|
196
|
+
|
|
197
|
+
.jp-CommandsReferenceWidget-table th:nth-child(5),
|
|
198
|
+
.jp-CommandsReferenceWidget-table td:nth-child(5) {
|
|
199
|
+
width: 15%;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Adaptive columns - keyed to the panel width, not the viewport. Narrower blocks come later so they win. */
|
|
203
|
+
@container (max-width: 1099px) {
|
|
204
|
+
.jp-CommandsReferenceWidget-table th:nth-child(3),
|
|
205
|
+
.jp-CommandsReferenceWidget-table td:nth-child(3) {
|
|
206
|
+
display: none;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.jp-CommandsReferenceWidget-table th:nth-child(1),
|
|
210
|
+
.jp-CommandsReferenceWidget-table td:nth-child(1) {
|
|
211
|
+
width: 29%;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.jp-CommandsReferenceWidget-table th:nth-child(2),
|
|
215
|
+
.jp-CommandsReferenceWidget-table td:nth-child(2) {
|
|
216
|
+
width: 21%;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.jp-CommandsReferenceWidget-table th:nth-child(4),
|
|
220
|
+
.jp-CommandsReferenceWidget-table td:nth-child(4) {
|
|
221
|
+
width: 32%;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.jp-CommandsReferenceWidget-table th:nth-child(5),
|
|
225
|
+
.jp-CommandsReferenceWidget-table td:nth-child(5) {
|
|
226
|
+
width: 18%;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@container (max-width: 899px) {
|
|
231
|
+
.jp-CommandsReferenceWidget-table th:nth-child(4),
|
|
232
|
+
.jp-CommandsReferenceWidget-table td:nth-child(4) {
|
|
233
|
+
display: none;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.jp-CommandsReferenceWidget-table th:nth-child(1),
|
|
237
|
+
.jp-CommandsReferenceWidget-table td:nth-child(1) {
|
|
238
|
+
width: 47%;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.jp-CommandsReferenceWidget-table th:nth-child(2),
|
|
242
|
+
.jp-CommandsReferenceWidget-table td:nth-child(2) {
|
|
243
|
+
width: 32%;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.jp-CommandsReferenceWidget-table th:nth-child(5),
|
|
247
|
+
.jp-CommandsReferenceWidget-table td:nth-child(5) {
|
|
248
|
+
width: 21%;
|
|
249
|
+
}
|
|
250
|
+
}
|