xtralab 0.13.0 → 0.14.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.
- package/README.md +1 -1
- package/lib/about/icons.d.ts +7 -0
- package/lib/about/icons.js +11 -0
- package/lib/about/index.d.ts +13 -0
- package/lib/about/index.js +81 -0
- package/lib/editorBreadcrumbs/widget.js +5 -0
- package/lib/fileBrowser/commands.d.ts +4 -2
- package/lib/fileBrowser/commands.js +27 -3
- package/lib/fileBrowser/fileBrowser.js +65 -0
- package/lib/fileBrowser/widget.d.ts +27 -0
- package/lib/fileBrowser/widget.js +18 -0
- package/lib/index.js +2 -0
- package/lib/omnibox/index.d.ts +5 -3
- package/lib/omnibox/index.js +17 -4
- package/lib/omnibox/model.d.ts +6 -0
- package/lib/omnibox/model.js +67 -12
- package/lib/omnibox/recents.d.ts +9 -2
- package/lib/omnibox/recents.js +40 -15
- package/lib/omnibox/widget.d.ts +6 -0
- package/lib/omnibox/widget.js +3 -2
- package/logo.svg +63 -0
- package/package.json +4 -2
- package/schema/about.json +24 -0
- package/src/about/icons.ts +13 -0
- package/src/about/index.tsx +125 -0
- package/src/editorBreadcrumbs/widget.ts +5 -0
- package/src/fileBrowser/commands.ts +32 -2
- package/src/fileBrowser/fileBrowser.tsx +64 -0
- package/src/fileBrowser/widget.tsx +47 -0
- package/src/index.ts +2 -0
- package/src/omnibox/index.ts +19 -4
- package/src/omnibox/model.ts +73 -11
- package/src/omnibox/recents.ts +53 -25
- package/src/omnibox/widget.tsx +9 -1
- package/src/svg.d.ts +4 -0
- package/style/about.css +66 -0
- package/style/chrome.css +8 -50
- package/style/index.css +1 -0
- package/style/index.js +1 -0
package/lib/omnibox/model.js
CHANGED
|
@@ -51,11 +51,11 @@ export function computeSections(options) {
|
|
|
51
51
|
* Run a command and record the use on success, so failed commands never enter
|
|
52
52
|
* the recents list.
|
|
53
53
|
*/
|
|
54
|
-
function executeCommand(commands, recents, id) {
|
|
54
|
+
function executeCommand(commands, recents, id, args) {
|
|
55
55
|
void commands
|
|
56
|
-
.execute(id)
|
|
56
|
+
.execute(id, args)
|
|
57
57
|
.then(() => {
|
|
58
|
-
recents === null || recents === void 0 ? void 0 : recents.touch('command', id);
|
|
58
|
+
recents === null || recents === void 0 ? void 0 : recents.touch('command', id, args);
|
|
59
59
|
})
|
|
60
60
|
.catch(reason => {
|
|
61
61
|
console.error(`xtralab omnibox: command "${id}" failed`, reason);
|
|
@@ -80,16 +80,16 @@ function openFile(commands, recents, path) {
|
|
|
80
80
|
* accessors can assume a context — e.g. an active notebook — the omnibox
|
|
81
81
|
* doesn't provide).
|
|
82
82
|
*/
|
|
83
|
-
function commandDisplay(commands, id) {
|
|
83
|
+
function commandDisplay(commands, id, args) {
|
|
84
84
|
if (!commands.hasCommand(id)) {
|
|
85
85
|
return null;
|
|
86
86
|
}
|
|
87
87
|
try {
|
|
88
|
-
const label = commands.label(id);
|
|
89
|
-
if (!label || !commands.isVisible(id)) {
|
|
88
|
+
const label = commands.label(id, args);
|
|
89
|
+
if (!label || !commands.isVisible(id, args)) {
|
|
90
90
|
return null;
|
|
91
91
|
}
|
|
92
|
-
return { label, caption: commands.caption(id) };
|
|
92
|
+
return { label, caption: commands.caption(id, args) };
|
|
93
93
|
}
|
|
94
94
|
catch (_a) {
|
|
95
95
|
return null;
|
|
@@ -103,6 +103,7 @@ function commandDisplay(commands, id) {
|
|
|
103
103
|
* unavailable — file entries are shown as recorded).
|
|
104
104
|
*/
|
|
105
105
|
function buildRecentItems(options, mode) {
|
|
106
|
+
var _a;
|
|
106
107
|
const { commands, docRegistry, files, recents } = options;
|
|
107
108
|
if (!recents) {
|
|
108
109
|
return [];
|
|
@@ -112,18 +113,18 @@ function buildRecentItems(options, mode) {
|
|
|
112
113
|
const items = [];
|
|
113
114
|
for (const entry of recents.entries(kind)) {
|
|
114
115
|
if (entry.kind === 'command') {
|
|
115
|
-
const display = commandDisplay(commands, entry.id);
|
|
116
|
+
const display = commandDisplay(commands, entry.id, entry.args);
|
|
116
117
|
if (!display) {
|
|
117
118
|
continue;
|
|
118
119
|
}
|
|
119
120
|
items.push({
|
|
120
121
|
kind: 'command',
|
|
121
|
-
key: `recent:command:${entry.id}`,
|
|
122
|
+
key: `recent:command:${entry.id}:${JSON.stringify((_a = entry.args) !== null && _a !== void 0 ? _a : null)}`,
|
|
122
123
|
label: display.label,
|
|
123
124
|
matchIndices: [],
|
|
124
125
|
caption: display.caption || undefined,
|
|
125
126
|
execute: () => {
|
|
126
|
-
executeCommand(commands, recents, entry.id);
|
|
127
|
+
executeCommand(commands, recents, entry.id, entry.args);
|
|
127
128
|
}
|
|
128
129
|
});
|
|
129
130
|
}
|
|
@@ -145,11 +146,65 @@ function buildRecentItems(options, mode) {
|
|
|
145
146
|
}
|
|
146
147
|
return items;
|
|
147
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Match the query against the palette's items first — their labels carry each
|
|
151
|
+
* item's args (one "Use Theme: …" per theme, one row per font-size key), which
|
|
152
|
+
* a raw registry scan can never produce — then against registry commands the
|
|
153
|
+
* palette doesn't present. Palette-covered ids are skipped in the registry
|
|
154
|
+
* pass so a command never also surfaces under its argless label.
|
|
155
|
+
*/
|
|
148
156
|
function matchCommands(options, term) {
|
|
149
|
-
const { commands, recents } = options;
|
|
157
|
+
const { commands, paletteItems, recents } = options;
|
|
150
158
|
const query = term.toLowerCase();
|
|
151
159
|
const scored = [];
|
|
160
|
+
const inPalette = new Set();
|
|
161
|
+
const seenKeys = new Set();
|
|
162
|
+
for (const item of paletteItems) {
|
|
163
|
+
inPalette.add(item.command);
|
|
164
|
+
const { command: id, args } = item;
|
|
165
|
+
let label = '';
|
|
166
|
+
let visible = true;
|
|
167
|
+
let caption = '';
|
|
168
|
+
try {
|
|
169
|
+
// An item's accessors can throw if the command assumes a context (e.g.
|
|
170
|
+
// an active notebook) the omnibox doesn't provide; skip those.
|
|
171
|
+
label = item.label;
|
|
172
|
+
visible = item.isVisible;
|
|
173
|
+
caption = item.caption;
|
|
174
|
+
}
|
|
175
|
+
catch (_a) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (!label || !visible) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const key = `command:${id}:${JSON.stringify(args)}`;
|
|
182
|
+
if (seenKeys.has(key)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
seenKeys.add(key);
|
|
186
|
+
const match = StringExt.matchSumOfSquares(label.toLowerCase(), query);
|
|
187
|
+
if (!match) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
scored.push({
|
|
191
|
+
score: match.score,
|
|
192
|
+
item: {
|
|
193
|
+
kind: 'command',
|
|
194
|
+
key,
|
|
195
|
+
label,
|
|
196
|
+
matchIndices: match.indices,
|
|
197
|
+
caption: caption || undefined,
|
|
198
|
+
execute: () => {
|
|
199
|
+
executeCommand(commands, recents, id, args);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
152
204
|
for (const id of commands.listCommands()) {
|
|
205
|
+
if (inPalette.has(id)) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
153
208
|
let label = '';
|
|
154
209
|
let visible = true;
|
|
155
210
|
let caption = '';
|
|
@@ -160,7 +215,7 @@ function matchCommands(options, term) {
|
|
|
160
215
|
visible = commands.isVisible(id);
|
|
161
216
|
caption = commands.caption(id);
|
|
162
217
|
}
|
|
163
|
-
catch (
|
|
218
|
+
catch (_b) {
|
|
164
219
|
continue;
|
|
165
220
|
}
|
|
166
221
|
if (!label || !visible) {
|
package/lib/omnibox/recents.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IStateDB } from '@jupyterlab/statedb';
|
|
2
|
+
import type { ReadonlyPartialJSONObject } from '@lumino/coreutils';
|
|
2
3
|
/**
|
|
3
4
|
* Kinds of omnibox results whose uses are recorded.
|
|
4
5
|
*/
|
|
@@ -12,6 +13,11 @@ interface IRecentEntry {
|
|
|
12
13
|
* The command id, or the workspace-relative file path.
|
|
13
14
|
*/
|
|
14
15
|
id: string;
|
|
16
|
+
/**
|
|
17
|
+
* Arguments the command ran with, for palette-style entries whose label
|
|
18
|
+
* and behavior depend on them (e.g. one "Use Theme: …" per theme).
|
|
19
|
+
*/
|
|
20
|
+
args?: ReadonlyPartialJSONObject;
|
|
15
21
|
}
|
|
16
22
|
/**
|
|
17
23
|
* A most-recently-used list of omnibox results, generic over the result kind
|
|
@@ -34,9 +40,10 @@ export declare class OmniboxRecents {
|
|
|
34
40
|
*/
|
|
35
41
|
entries(kind?: RecentKind): IRecentEntry[];
|
|
36
42
|
/**
|
|
37
|
-
* Record a use of `id
|
|
43
|
+
* Record a use of `id` (with the args it ran with, if any), moving it to
|
|
44
|
+
* the front of its kind's list.
|
|
38
45
|
*/
|
|
39
|
-
touch(kind: RecentKind, id: string): void;
|
|
46
|
+
touch(kind: RecentKind, id: string, args?: ReadonlyPartialJSONObject): void;
|
|
40
47
|
/**
|
|
41
48
|
* Load the persisted list. Entries recorded before the load completes stay
|
|
42
49
|
* at the front, ahead of the restored history.
|
package/lib/omnibox/recents.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity of an entry: two uses only match when kind, id and args all do,
|
|
3
|
+
* so runs of one command with different args stay distinct entries.
|
|
4
|
+
*/
|
|
5
|
+
function entryKey(entry) {
|
|
6
|
+
var _a;
|
|
7
|
+
return `${entry.kind}\0${entry.id}\0${JSON.stringify((_a = entry.args) !== null && _a !== void 0 ? _a : null)}`;
|
|
8
|
+
}
|
|
1
9
|
/**
|
|
2
10
|
* State database key the recents list is persisted under.
|
|
3
11
|
*/
|
|
@@ -45,11 +53,14 @@ export class OmniboxRecents {
|
|
|
45
53
|
: [...this._entries];
|
|
46
54
|
}
|
|
47
55
|
/**
|
|
48
|
-
* Record a use of `id
|
|
56
|
+
* Record a use of `id` (with the args it ran with, if any), moving it to
|
|
57
|
+
* the front of its kind's list.
|
|
49
58
|
*/
|
|
50
|
-
touch(kind, id) {
|
|
51
|
-
const
|
|
52
|
-
|
|
59
|
+
touch(kind, id, args) {
|
|
60
|
+
const entry = args === undefined ? { kind, id } : { kind, id, args };
|
|
61
|
+
const key = entryKey(entry);
|
|
62
|
+
const rest = this._entries.filter(existing => entryKey(existing) !== key);
|
|
63
|
+
this._entries = this._trim([entry, ...rest]);
|
|
53
64
|
this._save();
|
|
54
65
|
}
|
|
55
66
|
/**
|
|
@@ -68,16 +79,31 @@ export class OmniboxRecents {
|
|
|
68
79
|
return;
|
|
69
80
|
}
|
|
70
81
|
const persisted = Array.isArray(fetched)
|
|
71
|
-
? fetched.filter((entry) =>
|
|
72
|
-
typeof entry
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
? fetched.filter((entry) => {
|
|
83
|
+
if (!entry || typeof entry !== 'object') {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const candidate = entry;
|
|
87
|
+
if (candidate.kind !== 'command' && candidate.kind !== 'file') {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (typeof candidate.id !== 'string') {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const args = candidate.args;
|
|
94
|
+
return (args === undefined ||
|
|
95
|
+
(typeof args === 'object' && args !== null && !Array.isArray(args)));
|
|
96
|
+
})
|
|
76
97
|
: [];
|
|
98
|
+
const known = new Set(this._entries.map(entryKey));
|
|
77
99
|
const merged = [...this._entries];
|
|
78
100
|
for (const entry of persisted) {
|
|
79
|
-
|
|
80
|
-
|
|
101
|
+
const key = entryKey(entry);
|
|
102
|
+
if (!known.has(key)) {
|
|
103
|
+
known.add(key);
|
|
104
|
+
merged.push(entry.args === undefined
|
|
105
|
+
? { kind: entry.kind, id: entry.id }
|
|
106
|
+
: { kind: entry.kind, id: entry.id, args: entry.args });
|
|
81
107
|
}
|
|
82
108
|
}
|
|
83
109
|
this._entries = this._trim(merged);
|
|
@@ -98,10 +124,9 @@ export class OmniboxRecents {
|
|
|
98
124
|
if (!this._state) {
|
|
99
125
|
return;
|
|
100
126
|
}
|
|
101
|
-
const value = this._entries.map(entry =>
|
|
102
|
-
kind: entry.kind,
|
|
103
|
-
id: entry.id
|
|
104
|
-
}));
|
|
127
|
+
const value = this._entries.map(entry => entry.args === undefined
|
|
128
|
+
? { kind: entry.kind, id: entry.id }
|
|
129
|
+
: { kind: entry.kind, id: entry.id, args: entry.args });
|
|
105
130
|
this._state.save(STATE_KEY, value).catch(reason => {
|
|
106
131
|
console.error('xtralab omnibox: failed to save recents', reason);
|
|
107
132
|
});
|
package/lib/omnibox/widget.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
|
2
2
|
import type { TranslationBundle } from '@jupyterlab/translation';
|
|
3
3
|
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
4
4
|
import type { CommandRegistry } from '@lumino/commands';
|
|
5
|
+
import type { CommandPalette } from '@lumino/widgets';
|
|
5
6
|
import type { IAgent } from '../launcher/agents';
|
|
6
7
|
import type { OmniboxRecents } from './recents';
|
|
7
8
|
/**
|
|
@@ -24,6 +25,11 @@ export declare namespace OmniboxWidget {
|
|
|
24
25
|
*/
|
|
25
26
|
interface IOptions {
|
|
26
27
|
commands: CommandRegistry;
|
|
28
|
+
/**
|
|
29
|
+
* Snapshot of the command palette's items, read once when the overlay
|
|
30
|
+
* opens; empty when the palette isn't available.
|
|
31
|
+
*/
|
|
32
|
+
paletteItems: ReadonlyArray<CommandPalette.IItem>;
|
|
27
33
|
docRegistry: DocumentRegistry;
|
|
28
34
|
/**
|
|
29
35
|
* Snapshot of the available agents, read once when the overlay opens.
|
package/lib/omnibox/widget.js
CHANGED
|
@@ -39,7 +39,7 @@ function renderIcon(icon) {
|
|
|
39
39
|
return icon ? (React.createElement(icon.react, { tag: "span", className: "jp-xtralab-Omnibox-itemIcon" })) : (React.createElement("span", { className: "jp-xtralab-Omnibox-itemIcon" }));
|
|
40
40
|
}
|
|
41
41
|
function OmniboxComponent(props) {
|
|
42
|
-
const { commands, docRegistry, agents, placeholder, initialQuery, recents, trans, onClose } = props;
|
|
42
|
+
const { commands, paletteItems, docRegistry, agents, placeholder, initialQuery, recents, trans, onClose } = props;
|
|
43
43
|
const [query, setQuery] = React.useState(initialQuery);
|
|
44
44
|
const [files, setFiles] = React.useState([]);
|
|
45
45
|
const [active, setActive] = React.useState(0);
|
|
@@ -63,12 +63,13 @@ function OmniboxComponent(props) {
|
|
|
63
63
|
const sections = React.useMemo(() => computeSections({
|
|
64
64
|
query,
|
|
65
65
|
commands,
|
|
66
|
+
paletteItems,
|
|
66
67
|
docRegistry,
|
|
67
68
|
agents,
|
|
68
69
|
files,
|
|
69
70
|
recents,
|
|
70
71
|
trans
|
|
71
|
-
}), [query, commands, docRegistry, agents, files, recents, trans]);
|
|
72
|
+
}), [query, commands, paletteItems, docRegistry, agents, files, recents, trans]);
|
|
72
73
|
const flat = React.useMemo(() => [
|
|
73
74
|
...sections.recent,
|
|
74
75
|
...sections.commands,
|
package/logo.svg
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -3 194 272" role="img" aria-labelledby="jp-xtralab-logo-title jp-xtralab-logo-desc">
|
|
2
|
+
<title id="jp-xtralab-logo-title">XtraLab flask icon</title>
|
|
3
|
+
<desc id="jp-xtralab-logo-desc">A clean, slightly tilted laboratory flask with a symmetrical constant-width dark frame, blue-to-pink liquid, bubbles, and rising pixel blocks.</desc>
|
|
4
|
+
|
|
5
|
+
<defs>
|
|
6
|
+
<linearGradient id="jp-xtralab-logo-liquid" x1="20" y1="154" x2="151" y2="231" gradientUnits="userSpaceOnUse">
|
|
7
|
+
<stop offset="0" stop-color="#00a6ed"/>
|
|
8
|
+
<stop offset=".29" stop-color="#0879ed"/>
|
|
9
|
+
<stop offset=".58" stop-color="#6548d2"/>
|
|
10
|
+
<stop offset=".8" stop-color="#b43baa"/>
|
|
11
|
+
<stop offset="1" stop-color="#f1357e"/>
|
|
12
|
+
</linearGradient>
|
|
13
|
+
<radialGradient id="jp-xtralab-logo-liquid-glow" cx="0" cy="0" r="1"
|
|
14
|
+
gradientTransform="translate(49 169) rotate(34) scale(75 49)"
|
|
15
|
+
gradientUnits="userSpaceOnUse">
|
|
16
|
+
<stop stop-color="#18b9ef" stop-opacity=".78"/>
|
|
17
|
+
<stop offset=".58" stop-color="#1688ef" stop-opacity=".2"/>
|
|
18
|
+
<stop offset="1" stop-color="#1688ef" stop-opacity="0"/>
|
|
19
|
+
</radialGradient>
|
|
20
|
+
<linearGradient id="jp-xtralab-logo-pixel-blue" x1="137" y1="13" x2="156" y2="32" gradientUnits="userSpaceOnUse">
|
|
21
|
+
<stop stop-color="#05aae9"/>
|
|
22
|
+
<stop offset="1" stop-color="#008be0"/>
|
|
23
|
+
</linearGradient>
|
|
24
|
+
<linearGradient id="jp-xtralab-logo-pixel-purple" x1="154" y1="39" x2="178" y2="61" gradientUnits="userSpaceOnUse">
|
|
25
|
+
<stop stop-color="#6242c0"/>
|
|
26
|
+
<stop offset="1" stop-color="#923ba8"/>
|
|
27
|
+
</linearGradient>
|
|
28
|
+
<linearGradient id="jp-xtralab-logo-pixel-pink" x1="166" y1="0" x2="183" y2="17" gradientUnits="userSpaceOnUse">
|
|
29
|
+
<stop stop-color="#8040bd"/>
|
|
30
|
+
<stop offset="1" stop-color="#ad3c9b"/>
|
|
31
|
+
</linearGradient>
|
|
32
|
+
<clipPath id="jp-xtralab-logo-chamber" clipPathUnits="userSpaceOnUse">
|
|
33
|
+
<path d="M64 61h36v43c0 10 2 18 8 27l42 73c5 8 5 18 0 26-5 8-13 12-23 13H37c-10-1-18-5-23-13-5-8-5-18 0-26l42-73c6-9 8-17 8-27Z"/>
|
|
34
|
+
</clipPath>
|
|
35
|
+
</defs>
|
|
36
|
+
|
|
37
|
+
<!-- The flask is constructed symmetrically around x=82, then given a slight logo-matching tilt. -->
|
|
38
|
+
<g transform="rotate(9 82 232)">
|
|
39
|
+
<g clip-path="url(#jp-xtralab-logo-chamber)">
|
|
40
|
+
<path fill="url(#jp-xtralab-logo-liquid)"
|
|
41
|
+
d="M8 207c17-25 34-52 55-60 16-6 29-2 42 5 14 8 27 8 43-4l13 65c4 18-8 35-27 37H30c-20-2-30-23-22-43Z"/>
|
|
42
|
+
<path fill="url(#jp-xtralab-logo-liquid-glow)"
|
|
43
|
+
d="M8 207c17-25 34-52 55-60 16-6 29-2 42 5 14 8 27 8 43-4l13 65c4 18-8 35-27 37H30c-20-2-30-23-22-43Z"/>
|
|
44
|
+
</g>
|
|
45
|
+
|
|
46
|
+
<!-- A straight integrated mouth joins the widened chimney without a separate rim element. -->
|
|
47
|
+
<path d="M56 55v49c0 12-2 21-8 31L9 201c-7 10-7 23-1 33s16 15 28 16h92c12-1 22-6 28-16s6-23-1-33l-39-66c-6-10-8-19-8-31V55H56Z"
|
|
48
|
+
fill="none" stroke="#07111d" stroke-width="16" stroke-linejoin="round" stroke-linecap="round"/>
|
|
49
|
+
<path d="M56 55v49c0 12-2 21-8 31L9 201c-7 10-7 23-1 33s16 15 28 16h92c12-1 22-6 28-16s6-23-1-33l-39-66c-6-10-8-19-8-31V55H56Z"
|
|
50
|
+
fill="none" stroke="#263b57" stroke-width="12" stroke-linejoin="round" stroke-linecap="round"/>
|
|
51
|
+
|
|
52
|
+
<circle cx="82" cy="80" r="4.7" fill="#009fe9" stroke="#0b1d35" stroke-width=".7"/>
|
|
53
|
+
<circle cx="82" cy="104" r="6.2" fill="#078ae9" stroke="#0b1d35" stroke-width=".7"/>
|
|
54
|
+
<circle cx="82" cy="128" r="7.2" fill="#3256d9" stroke="#0b1d35" stroke-width=".7"/>
|
|
55
|
+
</g>
|
|
56
|
+
|
|
57
|
+
<g stroke="#17233c" stroke-width=".75">
|
|
58
|
+
<rect x="116" y="22" width="13" height="13" rx="2" fill="#7342bd"/>
|
|
59
|
+
<rect x="137" y="13" width="19" height="19" rx="1.5" fill="url(#jp-xtralab-logo-pixel-blue)"/>
|
|
60
|
+
<rect x="154" y="39" width="24" height="22" rx="1.5" fill="url(#jp-xtralab-logo-pixel-purple)"/>
|
|
61
|
+
<rect x="166" y=".5" width="17" height="17" rx="1.5" fill="url(#jp-xtralab-logo-pixel-pink)"/>
|
|
62
|
+
</g>
|
|
63
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xtralab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "An opinionated JupyterLab meta-package that bundles a curated set of extensions, ships a path-first file browser, renders jupyterlab-git's text, notebook and image diffs with its own viewers, and applies a quieter default workspace configuration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
|
|
22
22
|
"style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
|
|
23
23
|
"src/**/*.{ts,tsx}",
|
|
24
|
-
"schema/*.json"
|
|
24
|
+
"schema/*.json",
|
|
25
|
+
"logo.svg"
|
|
25
26
|
],
|
|
26
27
|
"main": "lib/index.js",
|
|
27
28
|
"types": "lib/index.d.ts",
|
|
@@ -141,6 +142,7 @@
|
|
|
141
142
|
"@jupyterlab/git:notebook-diff",
|
|
142
143
|
"@jupyterlab/git:plain-text-diff",
|
|
143
144
|
"@jupyterlab/git:image-diff",
|
|
145
|
+
"@jupyterlab/help-extension:about",
|
|
144
146
|
"@jupyterlab/launcher-extension"
|
|
145
147
|
]
|
|
146
148
|
},
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"jupyter.lab.menus": {
|
|
3
|
+
"main": [
|
|
4
|
+
{
|
|
5
|
+
"id": "jp-mainmenu-help",
|
|
6
|
+
"items": [
|
|
7
|
+
{
|
|
8
|
+
"command": "help:about",
|
|
9
|
+
"rank": 0
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"type": "separator",
|
|
13
|
+
"rank": 0
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"title": "xtralab About",
|
|
20
|
+
"description": "The About dialog showing the running xtralab and JupyterLab versions.",
|
|
21
|
+
"type": "object",
|
|
22
|
+
"properties": {},
|
|
23
|
+
"additionalProperties": false
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LabIcon } from '@jupyterlab/ui-components';
|
|
2
|
+
|
|
3
|
+
import logoSvgstr from '../../logo.svg';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The project logo mark — `logo.svg` at the repository root, the same file
|
|
7
|
+
* the README and the docs site use. The labextension build inlines it as a
|
|
8
|
+
* raw string, so the dialog always shows the current artwork.
|
|
9
|
+
*/
|
|
10
|
+
export const aboutLogoIcon = new LabIcon({
|
|
11
|
+
name: 'xtralab:about-logo',
|
|
12
|
+
svgstr: logoSvgstr
|
|
13
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JupyterFrontEnd,
|
|
3
|
+
JupyterFrontEndPlugin
|
|
4
|
+
} from '@jupyterlab/application';
|
|
5
|
+
import { Dialog, ICommandPalette, showDialog } from '@jupyterlab/apputils';
|
|
6
|
+
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
7
|
+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
|
|
8
|
+
import * as React from 'react';
|
|
9
|
+
|
|
10
|
+
import { aboutLogoIcon } from './icons';
|
|
11
|
+
|
|
12
|
+
const PLUGIN_ID = 'xtralab:about';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Upstream command id. Reusing it keeps the Help menu entry (contributed by
|
|
16
|
+
* `schema/about.json` at the same rank as upstream), the command palette
|
|
17
|
+
* muscle memory, and any external caller of `help:about` working.
|
|
18
|
+
*/
|
|
19
|
+
const CommandIDs = {
|
|
20
|
+
about: 'help:about'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const REPO_URL = 'https://github.com/jtpio/xtralab';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* An "About xtralab" dialog replacing the stock "About JupyterLab" one.
|
|
27
|
+
*
|
|
28
|
+
* The upstream `@jupyterlab/help-extension:about` plugin is disabled via
|
|
29
|
+
* `jupyterlab.disabledExtensions` (package.json), which removes both its
|
|
30
|
+
* `help:about` command and its Help menu contribution; this plugin registers
|
|
31
|
+
* the same command id and restores the menu entry through its own schema.
|
|
32
|
+
* The dialog leads with the xtralab version and keeps the underlying
|
|
33
|
+
* JupyterLab version visible underneath.
|
|
34
|
+
*/
|
|
35
|
+
const plugin: JupyterFrontEndPlugin<void> = {
|
|
36
|
+
id: PLUGIN_ID,
|
|
37
|
+
description: 'Show an About dialog with the xtralab and JupyterLab versions.',
|
|
38
|
+
autoStart: true,
|
|
39
|
+
requires: [ISettingRegistry],
|
|
40
|
+
optional: [ICommandPalette, ITranslator],
|
|
41
|
+
activate: (
|
|
42
|
+
app: JupyterFrontEnd,
|
|
43
|
+
settingRegistry: ISettingRegistry,
|
|
44
|
+
palette: ICommandPalette | null,
|
|
45
|
+
translator: ITranslator | null
|
|
46
|
+
): void => {
|
|
47
|
+
const { commands } = app;
|
|
48
|
+
const trans = (translator ?? nullTranslator).load('jupyterlab');
|
|
49
|
+
|
|
50
|
+
commands.addCommand(CommandIDs.about, {
|
|
51
|
+
label: trans.__('About %1', 'xtralab'),
|
|
52
|
+
describedBy: {
|
|
53
|
+
args: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
execute: async () => {
|
|
59
|
+
// The settings payload carries the version of the installed
|
|
60
|
+
// labextension (read server-side from its package.json), which is
|
|
61
|
+
// the release actually running — the page config only knows the
|
|
62
|
+
// JupyterLab version. 'N/A' is jupyterlab_server's not-found value.
|
|
63
|
+
const version = await settingRegistry
|
|
64
|
+
.load(PLUGIN_ID)
|
|
65
|
+
.then(settings =>
|
|
66
|
+
settings.version === 'N/A' ? '' : settings.version
|
|
67
|
+
)
|
|
68
|
+
.catch(() => '');
|
|
69
|
+
|
|
70
|
+
const title = (
|
|
71
|
+
<span className="jp-xtralab-About-header">
|
|
72
|
+
<aboutLogoIcon.react
|
|
73
|
+
className="jp-xtralab-About-logo"
|
|
74
|
+
height="56px"
|
|
75
|
+
width="56px"
|
|
76
|
+
/>
|
|
77
|
+
<div className="jp-xtralab-About-header-info">
|
|
78
|
+
<span className="jp-xtralab-About-wordmark">xtralab</span>
|
|
79
|
+
{version ? (
|
|
80
|
+
<span className="jp-xtralab-About-version">
|
|
81
|
+
{trans.__('Version %1', version)}
|
|
82
|
+
</span>
|
|
83
|
+
) : null}
|
|
84
|
+
</div>
|
|
85
|
+
</span>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const body = (
|
|
89
|
+
<div className="jp-xtralab-About-body">
|
|
90
|
+
<span className="jp-xtralab-About-basedOn">
|
|
91
|
+
{trans.__('Based on JupyterLab %1', app.version)}
|
|
92
|
+
</span>
|
|
93
|
+
<span className="jp-xtralab-About-externalLinks">
|
|
94
|
+
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
|
95
|
+
{trans.__('xtralab on GitHub')}
|
|
96
|
+
</a>
|
|
97
|
+
</span>
|
|
98
|
+
<span className="jp-xtralab-About-copyright">
|
|
99
|
+
{trans.__('© %1 Jeremy Tuloup', 2026)}
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return showDialog({
|
|
105
|
+
title,
|
|
106
|
+
body,
|
|
107
|
+
buttons: [
|
|
108
|
+
Dialog.cancelButton({
|
|
109
|
+
label: trans.__('Close')
|
|
110
|
+
})
|
|
111
|
+
]
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (palette) {
|
|
117
|
+
palette.addItem({
|
|
118
|
+
command: CommandIDs.about,
|
|
119
|
+
category: trans.__('Help')
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export default plugin;
|
|
@@ -6,6 +6,7 @@ import type { CommandRegistry } from '@lumino/commands';
|
|
|
6
6
|
import { Widget } from '@lumino/widgets';
|
|
7
7
|
|
|
8
8
|
const EDITOR_BREADCRUMBS_CLASS = 'jp-xtralab-EditorBreadcrumbs';
|
|
9
|
+
const TOOLBAR_SPACER_CLASS = 'jp-Toolbar-spacer';
|
|
9
10
|
const BREADCRUMBS_CONTAINER_CLASS = 'jp-BreadCrumbs-container';
|
|
10
11
|
const BREADCRUMBS_CONTENT_CLASS = 'jp-BreadCrumbs-content';
|
|
11
12
|
const ROOT_CLASS = 'jp-BreadCrumbs-home';
|
|
@@ -37,6 +38,10 @@ export class EditorBreadcrumbs extends Widget {
|
|
|
37
38
|
this._commands = options.commands;
|
|
38
39
|
this._trans = options.trans;
|
|
39
40
|
this.addClass(EDITOR_BREADCRUMBS_CLASS);
|
|
41
|
+
// The breadcrumbs stretch to fill the toolbar, so ReactiveToolbar's
|
|
42
|
+
// overflow math must count them as a spacer (2px) — measured at their
|
|
43
|
+
// stretched clientWidth they would evict every item into the popup.
|
|
44
|
+
this.addClass(TOOLBAR_SPACER_CLASS);
|
|
40
45
|
this.node.setAttribute('aria-label', this._trans.__('Editor file path'));
|
|
41
46
|
|
|
42
47
|
this._container = document.createElement('span');
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
downloadIcon,
|
|
20
20
|
editIcon,
|
|
21
21
|
fileIcon,
|
|
22
|
+
filterIcon,
|
|
22
23
|
IDisposableMenuItem,
|
|
23
24
|
newFolderIcon,
|
|
24
25
|
RankedMenu,
|
|
@@ -47,6 +48,7 @@ export namespace CommandIDs {
|
|
|
47
48
|
export const download = 'xtralab:download';
|
|
48
49
|
export const refresh = 'xtralab:refresh';
|
|
49
50
|
export const collapseAll = 'xtralab:collapse-all';
|
|
51
|
+
export const toggleFileFilter = 'xtralab:toggle-file-filter';
|
|
50
52
|
export const createNewDirectory = 'xtralab:create-new-directory';
|
|
51
53
|
export const newLauncher = 'xtralab:new-launcher';
|
|
52
54
|
export const revealPath = 'xtralab:reveal-path';
|
|
@@ -484,6 +486,24 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
|
|
|
484
486
|
}
|
|
485
487
|
});
|
|
486
488
|
|
|
489
|
+
commands.addCommand(CommandIDs.toggleFileFilter, {
|
|
490
|
+
label: trans.__('Toggle File Filter'),
|
|
491
|
+
caption: trans.__('Show or hide the file filter'),
|
|
492
|
+
icon: filterIcon.bindprops({ stylesheet: 'menuItem' }),
|
|
493
|
+
isToggled: () => browser.fileFilterVisible,
|
|
494
|
+
execute: () => {
|
|
495
|
+
browser.toggleFileFilter();
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Refresh the toggle's state however the filter is shown or hidden —
|
|
500
|
+
// the toolbar button, or the auto-show that kicks in when typing with
|
|
501
|
+
// the tree focused opens a search session.
|
|
502
|
+
const onFilterVisibleChanged = (): void => {
|
|
503
|
+
commands.notifyCommandChanged(CommandIDs.toggleFileFilter);
|
|
504
|
+
};
|
|
505
|
+
browser.fileFilterVisibleChanged.connect(onFilterVisibleChanged);
|
|
506
|
+
|
|
487
507
|
// Public reveal seam. Other plugins (editor breadcrumbs, future
|
|
488
508
|
// "reveal in tree" actions) call this command rather than depending
|
|
489
509
|
// on the file browser widget directly. Activating the sidebar is part
|
|
@@ -591,6 +611,7 @@ export function registerCommands(opts: IRegisterCommandsOptions): () => void {
|
|
|
591
611
|
return () => {
|
|
592
612
|
app.contextMenu.opened.disconnect(updateOpenWithMenu);
|
|
593
613
|
app.shell.currentChanged?.disconnect(onCurrentChanged);
|
|
614
|
+
browser.fileFilterVisibleChanged.disconnect(onFilterVisibleChanged);
|
|
594
615
|
paletteItem?.dispose();
|
|
595
616
|
};
|
|
596
617
|
}
|
|
@@ -605,13 +626,14 @@ export namespace ToolbarNames {
|
|
|
605
626
|
export const newDirectory = 'new-directory';
|
|
606
627
|
export const refresh = 'refresh';
|
|
607
628
|
export const collapseAll = 'collapse-all';
|
|
629
|
+
export const toggleFileFilter = 'toggle-file-filter';
|
|
608
630
|
}
|
|
609
631
|
|
|
610
632
|
/**
|
|
611
633
|
* Populate the file browser's toolbar with the buttons that mirror the
|
|
612
634
|
* default JupyterLab file browser: a "+" launcher button, a new-folder
|
|
613
|
-
* button,
|
|
614
|
-
*
|
|
635
|
+
* button, a refresh button, a collapse-all button, and the file filter
|
|
636
|
+
* toggle.
|
|
615
637
|
*/
|
|
616
638
|
export function populateToolbar(opts: {
|
|
617
639
|
app: JupyterFrontEnd;
|
|
@@ -652,4 +674,12 @@ export function populateToolbar(opts: {
|
|
|
652
674
|
label: ''
|
|
653
675
|
})
|
|
654
676
|
);
|
|
677
|
+
browser.toolbar.addItem(
|
|
678
|
+
ToolbarNames.toggleFileFilter,
|
|
679
|
+
new CommandToolbarButton({
|
|
680
|
+
commands,
|
|
681
|
+
id: CommandIDs.toggleFileFilter,
|
|
682
|
+
label: ''
|
|
683
|
+
})
|
|
684
|
+
);
|
|
655
685
|
}
|