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.
@@ -93,10 +93,21 @@ const GIT_REPO_PATH = '';
93
93
  */
94
94
  const FILE_TREE_TAG = 'file-tree-container';
95
95
 
96
+ /**
97
+ * Injected into the tree's shadow root. The second rule hides the search
98
+ * box unless the host carries the visibility marker set by the filter
99
+ * bridge effect below — `@pierre/trees` always renders the box when
100
+ * `search` is enabled, and its stylesheet lives in the shadow root where
101
+ * outside CSS cannot reach.
102
+ */
96
103
  const FILE_TREE_UNSAFE_CSS =
97
104
  '[data-type="item"][data-item-selected="true"] ' +
98
105
  '[data-item-section="spacing-item"] {' +
99
106
  'border-left-color: transparent;' +
107
+ '}' +
108
+ ':host(:not([data-xtralab-filter-visible])) ' +
109
+ '[data-file-tree-search-container] {' +
110
+ 'display: none;' +
100
111
  '}';
101
112
 
102
113
  interface IFileBrowserProps {
@@ -748,6 +759,17 @@ export function FileBrowserComponent(
748
759
  contentsManager.fileChanged.connect(onContentsFileChanged);
749
760
 
750
761
  const unsubscribe = model.subscribe(() => {
762
+ // Search-driven expansion must not trigger fetches: the tree
763
+ // auto-expands every directory whose path matches the query, and
764
+ // treating those as user expansions would recursively fetch every
765
+ // matching subtree — a single common letter can walk the whole
766
+ // workspace, node_modules included. Closing the search restores
767
+ // the pre-search expansion state, and a directory clicked in the
768
+ // results is toggled after the session closes, so real expansions
769
+ // are still fetched the moment the session ends.
770
+ if (model.isSearchOpen()) {
771
+ return;
772
+ }
751
773
  knownDirs.forEach((state, canonicalPath) => {
752
774
  if (state !== 'unloaded') {
753
775
  return;
@@ -848,6 +870,48 @@ export function FileBrowserComponent(
848
870
  };
849
871
  }, [model, widget]);
850
872
 
873
+ // Bridge the filter-box visibility between the widget and the tree. The
874
+ // widget owns the flag; applying it stamps the marker attribute the
875
+ // unsafeCSS rule keys on and opens or closes the model's search session
876
+ // (opening focuses the input, closing clears any active filter). The
877
+ // model subscription covers the reverse direction: typing a printable
878
+ // character while the tree has focus opens a session on the tree's own
879
+ // initiative, and the box must surface for that session to be usable.
880
+ React.useEffect(() => {
881
+ if (widget === undefined) {
882
+ return;
883
+ }
884
+ const apply = (visible: boolean): void => {
885
+ const host = model.getFileTreeContainer();
886
+ if (host !== undefined) {
887
+ if (visible) {
888
+ host.dataset.xtralabFilterVisible = 'true';
889
+ } else {
890
+ delete host.dataset.xtralabFilterVisible;
891
+ }
892
+ }
893
+ if (visible && !model.isSearchOpen()) {
894
+ model.openSearch();
895
+ } else if (!visible && model.isSearchOpen()) {
896
+ model.closeSearch();
897
+ }
898
+ };
899
+ apply(widget.fileFilterVisible);
900
+ const visibleSlot = (sender: unknown, visible: boolean): void => {
901
+ apply(visible);
902
+ };
903
+ widget.fileFilterVisibleChanged.connect(visibleSlot);
904
+ const unsubscribe = model.subscribe(() => {
905
+ if (model.isSearchOpen() && !widget.fileFilterVisible) {
906
+ widget.setFileFilterVisible(true);
907
+ }
908
+ });
909
+ return () => {
910
+ widget.fileFilterVisibleChanged.disconnect(visibleSlot);
911
+ unsubscribe();
912
+ };
913
+ }, [model, widget]);
914
+
851
915
  const wrapperRef = React.useRef<HTMLDivElement | null>(null);
852
916
 
853
917
  React.useEffect(() => {
@@ -99,6 +99,21 @@ export interface IXtralabFileBrowser {
99
99
  */
100
100
  readonly collapseAllRequested: ISignal<IXtralabFileBrowser, void>;
101
101
 
102
+ /**
103
+ * Whether the filter box above the tree is shown. Mirrors the default
104
+ * file browser's `showFileFilter`: hidden on startup and flipped by the
105
+ * toolbar toggle. Also forced to `true` when the tree opens a search
106
+ * session on its own — typing a letter while the tree has focus.
107
+ */
108
+ readonly fileFilterVisible: boolean;
109
+
110
+ /**
111
+ * Emits when {@link fileFilterVisible} changes. The React component
112
+ * listens to show or hide the filter box; the toggle command listens
113
+ * to refresh its toggled state.
114
+ */
115
+ readonly fileFilterVisibleChanged: ISignal<IXtralabFileBrowser, boolean>;
116
+
102
117
  /**
103
118
  * Trigger a refresh of every loaded directory in the tree.
104
119
  */
@@ -130,6 +145,16 @@ export interface IXtralabFileBrowser {
130
145
  * Ask the tree to collapse every currently expanded folder.
131
146
  */
132
147
  collapseAll(): void;
148
+
149
+ /**
150
+ * Show or hide the filter box above the tree.
151
+ */
152
+ setFileFilterVisible(visible: boolean): void;
153
+
154
+ /**
155
+ * Toggle the filter box above the tree.
156
+ */
157
+ toggleFileFilter(): void;
133
158
  }
134
159
 
135
160
  /**
@@ -201,6 +226,14 @@ export class XtralabFileBrowser extends Widget implements IXtralabFileBrowser {
201
226
  return this._collapseAllRequested;
202
227
  }
203
228
 
229
+ get fileFilterVisible(): boolean {
230
+ return this._fileFilterVisible;
231
+ }
232
+
233
+ get fileFilterVisibleChanged(): ISignal<this, boolean> {
234
+ return this._fileFilterVisibleChanged;
235
+ }
236
+
204
237
  /**
205
238
  * Update the cached selection. Called from the React tree when the
206
239
  * underlying `@pierre/trees` model emits a selection change.
@@ -235,6 +268,18 @@ export class XtralabFileBrowser extends Widget implements IXtralabFileBrowser {
235
268
  this._collapseAllRequested.emit();
236
269
  }
237
270
 
271
+ setFileFilterVisible(visible: boolean): void {
272
+ if (this._fileFilterVisible === visible) {
273
+ return;
274
+ }
275
+ this._fileFilterVisible = visible;
276
+ this._fileFilterVisibleChanged.emit(visible);
277
+ }
278
+
279
+ toggleFileFilter(): void {
280
+ this.setFileFilterVisible(!this._fileFilterVisible);
281
+ }
282
+
238
283
  private _contentsManager: Contents.IManager;
239
284
  private _docManager: IDocumentManager;
240
285
  private _onOpenFile: ((serverPath: string) => void) | undefined;
@@ -245,6 +290,8 @@ export class XtralabFileBrowser extends Widget implements IXtralabFileBrowser {
245
290
  private _revealRequested = new Signal<this, string>(this);
246
291
  private _rootRequested = new Signal<this, void>(this);
247
292
  private _collapseAllRequested = new Signal<this, void>(this);
293
+ private _fileFilterVisible = false;
294
+ private _fileFilterVisibleChanged = new Signal<this, boolean>(this);
248
295
  private _toolbar: Toolbar;
249
296
  private _content: XtralabFileTreeContent;
250
297
  }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
2
 
3
+ import aboutPlugin from './about';
3
4
  import agentSessionsPlugin from './agentSessions';
4
5
  import askAgentPlugin from './askAgent';
5
6
  import commandBarPlugin from './commandBar';
@@ -30,6 +31,7 @@ import walkthroughPlugin from './walkthrough';
30
31
  * they are spread in.
31
32
  */
32
33
  const plugins: JupyterFrontEndPlugin<unknown>[] = [
34
+ aboutPlugin,
33
35
  agentSessionsPlugin,
34
36
  askAgentPlugin,
35
37
  commandBarPlugin,
@@ -6,7 +6,7 @@ import { ICommandPalette } from '@jupyterlab/apputils';
6
6
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
7
7
  import { IStateDB } from '@jupyterlab/statedb';
8
8
  import { ITranslator, nullTranslator } from '@jupyterlab/translation';
9
- import { Widget } from '@lumino/widgets';
9
+ import { CommandPalette, Widget } from '@lumino/widgets';
10
10
 
11
11
  import { IAgentRegistry } from '../launcher/tokens';
12
12
 
@@ -19,9 +19,11 @@ const PLUGIN_ID = 'xtralab:omnibox';
19
19
  /**
20
20
  * The omnibox: a single launcher overlay that fuzzy-searches workspace files
21
21
  * (via jupyterlab-quickopen's gitignore-aware endpoint) and JupyterLab
22
- * commands, and routes a typed prompt to one of the configured agents (running
23
- * it in a fresh terminal through the launcher's `xtralab:start-agent:<id>`
24
- * commands). It is opened by the top-bar command bar.
22
+ * commands (the command palette's entries plus registry commands the palette
23
+ * doesn't list), and routes a typed prompt to one of the configured agents
24
+ * (running it in a fresh terminal through the launcher's
25
+ * `xtralab:start-agent:<id>` commands). It is opened by the top-bar command
26
+ * bar.
25
27
  *
26
28
  * Commands run and files opened through the overlay are remembered (persisted
27
29
  * in the state database) and offered again at the top while the query is
@@ -55,6 +57,18 @@ const plugin: JupyterFrontEndPlugin<IOmnibox> = {
55
57
  const trans = (translator ?? nullTranslator).load('jupyterlab');
56
58
  const placeholder = trans.__('Search files and commands, or ask an agent…');
57
59
 
60
+ // The palette's items carry per-item args (e.g. one "Use Theme: …" per
61
+ // theme) that a raw command-registry scan can't see, but `ICommandPalette`
62
+ // exposes no item list and the widget itself is unreachable through the
63
+ // shell (`LabShell.add` defers it until layout restore, after which the
64
+ // modal setting re-parents it out of the left area). The wrapper's
65
+ // `_palette` field is private in TypeScript only, so read it guarded.
66
+ const paletteItems = (): ReadonlyArray<CommandPalette.IItem> => {
67
+ const widget = (palette as unknown as { _palette?: unknown } | null)
68
+ ?._palette;
69
+ return widget instanceof CommandPalette ? [...widget.items] : [];
70
+ };
71
+
58
72
  const recents = new OmniboxRecents({ state });
59
73
 
60
74
  // Restore the persisted recents only after the configured cap is known:
@@ -100,6 +114,7 @@ const plugin: JupyterFrontEndPlugin<IOmnibox> = {
100
114
  close();
101
115
  const widget = new OmniboxWidget({
102
116
  commands,
117
+ paletteItems: paletteItems(),
103
118
  docRegistry,
104
119
  agents: agentRegistry ? agentRegistry.agents : [],
105
120
  placeholder,
@@ -3,6 +3,8 @@ import type { TranslationBundle } from '@jupyterlab/translation';
3
3
  import { fileIcon, LabIcon } from '@jupyterlab/ui-components';
4
4
  import { StringExt } from '@lumino/algorithm';
5
5
  import type { CommandRegistry } from '@lumino/commands';
6
+ import type { ReadonlyPartialJSONObject } from '@lumino/coreutils';
7
+ import type { CommandPalette } from '@lumino/widgets';
6
8
 
7
9
  import type { IAgent } from '../launcher/agents';
8
10
  import { agentCommandId } from '../launcher/tokens';
@@ -61,6 +63,11 @@ interface IOmniboxSections {
61
63
  interface IComputeOptions {
62
64
  query: string;
63
65
  commands: CommandRegistry;
66
+ /**
67
+ * The command palette's items, whose labels are computed with each item's
68
+ * args — the only form in which entries like "Use Theme: …" exist.
69
+ */
70
+ paletteItems: ReadonlyArray<CommandPalette.IItem>;
64
71
  docRegistry: DocumentRegistry;
65
72
  agents: IAgent[];
66
73
  files: string[];
@@ -131,12 +138,13 @@ export function computeSections(options: IComputeOptions): IOmniboxSections {
131
138
  function executeCommand(
132
139
  commands: CommandRegistry,
133
140
  recents: OmniboxRecents | null,
134
- id: string
141
+ id: string,
142
+ args?: ReadonlyPartialJSONObject
135
143
  ): void {
136
144
  void commands
137
- .execute(id)
145
+ .execute(id, args)
138
146
  .then(() => {
139
- recents?.touch('command', id);
147
+ recents?.touch('command', id, args);
140
148
  })
141
149
  .catch(reason => {
142
150
  console.error(`xtralab omnibox: command "${id}" failed`, reason);
@@ -169,17 +177,18 @@ function openFile(
169
177
  */
170
178
  function commandDisplay(
171
179
  commands: CommandRegistry,
172
- id: string
180
+ id: string,
181
+ args?: ReadonlyPartialJSONObject
173
182
  ): { label: string; caption: string } | null {
174
183
  if (!commands.hasCommand(id)) {
175
184
  return null;
176
185
  }
177
186
  try {
178
- const label = commands.label(id);
179
- if (!label || !commands.isVisible(id)) {
187
+ const label = commands.label(id, args);
188
+ if (!label || !commands.isVisible(id, args)) {
180
189
  return null;
181
190
  }
182
- return { label, caption: commands.caption(id) };
191
+ return { label, caption: commands.caption(id, args) };
183
192
  } catch {
184
193
  return null;
185
194
  }
@@ -207,18 +216,18 @@ function buildRecentItems(
207
216
  const items: IOmniboxItem[] = [];
208
217
  for (const entry of recents.entries(kind)) {
209
218
  if (entry.kind === 'command') {
210
- const display = commandDisplay(commands, entry.id);
219
+ const display = commandDisplay(commands, entry.id, entry.args);
211
220
  if (!display) {
212
221
  continue;
213
222
  }
214
223
  items.push({
215
224
  kind: 'command',
216
- key: `recent:command:${entry.id}`,
225
+ key: `recent:command:${entry.id}:${JSON.stringify(entry.args ?? null)}`,
217
226
  label: display.label,
218
227
  matchIndices: [],
219
228
  caption: display.caption || undefined,
220
229
  execute: () => {
221
- executeCommand(commands, recents, entry.id);
230
+ executeCommand(commands, recents, entry.id, entry.args);
222
231
  }
223
232
  });
224
233
  } else {
@@ -240,11 +249,64 @@ function buildRecentItems(
240
249
  return items;
241
250
  }
242
251
 
252
+ /**
253
+ * Match the query against the palette's items first — their labels carry each
254
+ * item's args (one "Use Theme: …" per theme, one row per font-size key), which
255
+ * a raw registry scan can never produce — then against registry commands the
256
+ * palette doesn't present. Palette-covered ids are skipped in the registry
257
+ * pass so a command never also surfaces under its argless label.
258
+ */
243
259
  function matchCommands(options: IComputeOptions, term: string): IOmniboxItem[] {
244
- const { commands, recents } = options;
260
+ const { commands, paletteItems, recents } = options;
245
261
  const query = term.toLowerCase();
246
262
  const scored: Array<{ score: number; item: IOmniboxItem }> = [];
263
+ const inPalette = new Set<string>();
264
+ const seenKeys = new Set<string>();
265
+ for (const item of paletteItems) {
266
+ inPalette.add(item.command);
267
+ const { command: id, args } = item;
268
+ let label = '';
269
+ let visible = true;
270
+ let caption = '';
271
+ try {
272
+ // An item's accessors can throw if the command assumes a context (e.g.
273
+ // an active notebook) the omnibox doesn't provide; skip those.
274
+ label = item.label;
275
+ visible = item.isVisible;
276
+ caption = item.caption;
277
+ } catch {
278
+ continue;
279
+ }
280
+ if (!label || !visible) {
281
+ continue;
282
+ }
283
+ const key = `command:${id}:${JSON.stringify(args)}`;
284
+ if (seenKeys.has(key)) {
285
+ continue;
286
+ }
287
+ seenKeys.add(key);
288
+ const match = StringExt.matchSumOfSquares(label.toLowerCase(), query);
289
+ if (!match) {
290
+ continue;
291
+ }
292
+ scored.push({
293
+ score: match.score,
294
+ item: {
295
+ kind: 'command',
296
+ key,
297
+ label,
298
+ matchIndices: match.indices,
299
+ caption: caption || undefined,
300
+ execute: () => {
301
+ executeCommand(commands, recents, id, args);
302
+ }
303
+ }
304
+ });
305
+ }
247
306
  for (const id of commands.listCommands()) {
307
+ if (inPalette.has(id)) {
308
+ continue;
309
+ }
248
310
  let label = '';
249
311
  let visible = true;
250
312
  let caption = '';
@@ -1,5 +1,5 @@
1
1
  import type { IStateDB } from '@jupyterlab/statedb';
2
- import type { PartialJSONObject } from '@lumino/coreutils';
2
+ import type { ReadonlyPartialJSONObject } from '@lumino/coreutils';
3
3
 
4
4
  /**
5
5
  * Kinds of omnibox results whose uses are recorded.
@@ -15,6 +15,19 @@ interface IRecentEntry {
15
15
  * The command id, or the workspace-relative file path.
16
16
  */
17
17
  id: string;
18
+ /**
19
+ * Arguments the command ran with, for palette-style entries whose label
20
+ * and behavior depend on them (e.g. one "Use Theme: …" per theme).
21
+ */
22
+ args?: ReadonlyPartialJSONObject;
23
+ }
24
+
25
+ /**
26
+ * Identity of an entry: two uses only match when kind, id and args all do,
27
+ * so runs of one command with different args stay distinct entries.
28
+ */
29
+ function entryKey(entry: IRecentEntry): string {
30
+ return `${entry.kind}\0${entry.id}\0${JSON.stringify(entry.args ?? null)}`;
18
31
  }
19
32
 
20
33
  /**
@@ -65,13 +78,15 @@ export class OmniboxRecents {
65
78
  }
66
79
 
67
80
  /**
68
- * Record a use of `id`, moving it to the front of its kind's list.
81
+ * Record a use of `id` (with the args it ran with, if any), moving it to
82
+ * the front of its kind's list.
69
83
  */
70
- touch(kind: RecentKind, id: string): void {
71
- const rest = this._entries.filter(
72
- entry => !(entry.kind === kind && entry.id === id)
73
- );
74
- this._entries = this._trim([{ kind, id }, ...rest]);
84
+ touch(kind: RecentKind, id: string, args?: ReadonlyPartialJSONObject): void {
85
+ const entry: IRecentEntry =
86
+ args === undefined ? { kind, id } : { kind, id, args };
87
+ const key = entryKey(entry);
88
+ const rest = this._entries.filter(existing => entryKey(existing) !== key);
89
+ this._entries = this._trim([entry, ...rest]);
75
90
  this._save();
76
91
  }
77
92
 
@@ -90,23 +105,35 @@ export class OmniboxRecents {
90
105
  return;
91
106
  }
92
107
  const persisted: IRecentEntry[] = Array.isArray(fetched)
93
- ? fetched.filter(
94
- (entry): entry is IRecentEntry =>
95
- !!entry &&
96
- typeof entry === 'object' &&
97
- ((entry as IRecentEntry).kind === 'command' ||
98
- (entry as IRecentEntry).kind === 'file') &&
99
- typeof (entry as IRecentEntry).id === 'string'
100
- )
108
+ ? fetched.filter((entry): entry is IRecentEntry => {
109
+ if (!entry || typeof entry !== 'object') {
110
+ return false;
111
+ }
112
+ const candidate = entry as IRecentEntry;
113
+ if (candidate.kind !== 'command' && candidate.kind !== 'file') {
114
+ return false;
115
+ }
116
+ if (typeof candidate.id !== 'string') {
117
+ return false;
118
+ }
119
+ const args: unknown = candidate.args;
120
+ return (
121
+ args === undefined ||
122
+ (typeof args === 'object' && args !== null && !Array.isArray(args))
123
+ );
124
+ })
101
125
  : [];
126
+ const known = new Set(this._entries.map(entryKey));
102
127
  const merged = [...this._entries];
103
128
  for (const entry of persisted) {
104
- if (
105
- !merged.some(
106
- existing => existing.kind === entry.kind && existing.id === entry.id
107
- )
108
- ) {
109
- merged.push({ kind: entry.kind, id: entry.id });
129
+ const key = entryKey(entry);
130
+ if (!known.has(key)) {
131
+ known.add(key);
132
+ merged.push(
133
+ entry.args === undefined
134
+ ? { kind: entry.kind, id: entry.id }
135
+ : { kind: entry.kind, id: entry.id, args: entry.args }
136
+ );
110
137
  }
111
138
  }
112
139
  this._entries = this._trim(merged);
@@ -128,10 +155,11 @@ export class OmniboxRecents {
128
155
  if (!this._state) {
129
156
  return;
130
157
  }
131
- const value = this._entries.map<PartialJSONObject>(entry => ({
132
- kind: entry.kind,
133
- id: entry.id
134
- }));
158
+ const value = this._entries.map<ReadonlyPartialJSONObject>(entry =>
159
+ entry.args === undefined
160
+ ? { kind: entry.kind, id: entry.id }
161
+ : { kind: entry.kind, id: entry.id, args: entry.args }
162
+ );
135
163
  this._state.save(STATE_KEY, value).catch(reason => {
136
164
  console.error('xtralab omnibox: failed to save recents', reason);
137
165
  });
@@ -2,6 +2,7 @@ import type { DocumentRegistry } from '@jupyterlab/docregistry';
2
2
  import type { TranslationBundle } from '@jupyterlab/translation';
3
3
  import { LabIcon, ReactWidget } from '@jupyterlab/ui-components';
4
4
  import type { CommandRegistry } from '@lumino/commands';
5
+ import type { CommandPalette } from '@lumino/widgets';
5
6
  import * as React from 'react';
6
7
 
7
8
  import type { IAgent } from '../launcher/agents';
@@ -68,6 +69,7 @@ function renderIcon(icon?: LabIcon): React.ReactNode {
68
69
  function OmniboxComponent(props: OmniboxWidget.IOptions): JSX.Element {
69
70
  const {
70
71
  commands,
72
+ paletteItems,
71
73
  docRegistry,
72
74
  agents,
73
75
  placeholder,
@@ -103,13 +105,14 @@ function OmniboxComponent(props: OmniboxWidget.IOptions): JSX.Element {
103
105
  computeSections({
104
106
  query,
105
107
  commands,
108
+ paletteItems,
106
109
  docRegistry,
107
110
  agents,
108
111
  files,
109
112
  recents,
110
113
  trans
111
114
  }),
112
- [query, commands, docRegistry, agents, files, recents, trans]
115
+ [query, commands, paletteItems, docRegistry, agents, files, recents, trans]
113
116
  );
114
117
  const flat = React.useMemo(
115
118
  () => [
@@ -334,6 +337,11 @@ export namespace OmniboxWidget {
334
337
  */
335
338
  export interface IOptions {
336
339
  commands: CommandRegistry;
340
+ /**
341
+ * Snapshot of the command palette's items, read once when the overlay
342
+ * opens; empty when the palette isn't available.
343
+ */
344
+ paletteItems: ReadonlyArray<CommandPalette.IItem>;
337
345
  docRegistry: DocumentRegistry;
338
346
  /**
339
347
  * Snapshot of the available agents, read once when the overlay opens.
package/src/svg.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ declare module '*.svg' {
2
+ const value: string;
3
+ export default value;
4
+ }
@@ -0,0 +1,66 @@
1
+ /*
2
+ * About dialog (xtralab:about). The header block is the dialog title, so it
3
+ * inherits the jp-Dialog-header type (size2/600 under chrome.css) — the
4
+ * wordmark scales up from there and the version line drops back to body
5
+ * size, weight and a muted color.
6
+ */
7
+
8
+ .jp-xtralab-About-header {
9
+ display: flex;
10
+ align-items: center;
11
+ gap: 14px;
12
+ }
13
+
14
+ .jp-xtralab-About-logo {
15
+ flex-shrink: 0;
16
+ }
17
+
18
+ .jp-xtralab-About-header-info {
19
+ display: flex;
20
+ flex-direction: column;
21
+ align-items: flex-start;
22
+ gap: 2px;
23
+ }
24
+
25
+ .jp-xtralab-About-wordmark {
26
+ font-size: var(--jp-ui-font-size3);
27
+ font-weight: 700;
28
+ letter-spacing: -0.01em;
29
+ line-height: 1.2;
30
+ }
31
+
32
+ .jp-xtralab-About-version {
33
+ font-size: var(--jp-ui-font-size1);
34
+ font-weight: 400;
35
+ color: var(--jp-ui-font-color2);
36
+ }
37
+
38
+ .jp-xtralab-About-body {
39
+ display: flex;
40
+ flex-direction: column;
41
+ min-width: 360px;
42
+ font-size: var(--jp-ui-font-size1);
43
+ color: var(--jp-ui-font-color1);
44
+ }
45
+
46
+ .jp-xtralab-About-basedOn {
47
+ color: var(--jp-ui-font-color2);
48
+ }
49
+
50
+ .jp-xtralab-About-externalLinks {
51
+ padding-top: 12px;
52
+ }
53
+
54
+ .jp-xtralab-About-externalLinks > a {
55
+ color: var(--jp-content-link-color);
56
+ text-decoration: none;
57
+ }
58
+
59
+ .jp-xtralab-About-externalLinks > a:hover {
60
+ text-decoration: underline;
61
+ }
62
+
63
+ .jp-xtralab-About-copyright {
64
+ padding-top: 12px;
65
+ color: var(--jp-ui-font-color3);
66
+ }
package/style/chrome.css CHANGED
@@ -206,6 +206,14 @@ body[data-jp-theme-light] diffs-container {
206
206
  scrollbar-color: auto;
207
207
  }
208
208
 
209
+ /* Carve-out: Lumino's drag cursor backdrop (the invisible scroll box that
210
+ follows the pointer while a splitter handle is held) hides its scrollbars
211
+ with scrollbar-width: none; the universal thin rule above out-specifies
212
+ that and would paint its thumbs as gray pills floating near the cursor. */
213
+ body[data-jp-theme-light] .lm-cursor-backdrop {
214
+ scrollbar-width: none;
215
+ }
216
+
209
217
  /* -----------------------------------------------------------------------------
210
218
  | Window chrome: seams, menu bar, activity strips, status bar, splitters
211
219
  |---------------------------------------------------------------------------- */
@@ -928,56 +936,6 @@ select.jp-mod-styled,
928
936
  border-color: color-mix(in srgb, var(--jp-border-color1) 55%, transparent);
929
937
  }
930
938
 
931
- /* -----------------------------------------------------------------------------
932
- | Toast notifications
933
- |---------------------------------------------------------------------------- */
934
-
935
- /*
936
- * Stock marks toast status with a 5px colored top strip. Rounded card with
937
- * hairline border and a slim colored left accent instead — same five
938
- * status tokens, modern grammar. Scoped under .jp-ThemedContainer (present
939
- * on both the corner toast container and the notification-center popup) so
940
- * these outrank react-toastify's lazy-loaded base CSS by specificity.
941
- * (.Toastify__toast is react-toastify's own BEM class, hence the
942
- * selector-class-pattern exemption.)
943
- */
944
-
945
- /* stylelint-disable selector-class-pattern */
946
- .jp-ThemedContainer .Toastify__toast {
947
- padding: 8px 10px;
948
- border: var(--jp-border-width) solid var(--jp-border-color2);
949
- border-radius: 10px;
950
- box-shadow:
951
- 0 1px 3px var(--jp-shadow-ambient-color),
952
- 0 8px 24px -6px var(--jp-shadow-penumbra-color);
953
- }
954
-
955
- .jp-ThemedContainer .Toastify__toast.jp-Notification-Toast-error {
956
- border-top: var(--jp-border-width) solid var(--jp-border-color2);
957
- border-left: 3px solid var(--jp-error-color1);
958
- }
959
-
960
- .jp-ThemedContainer .Toastify__toast.jp-Notification-Toast-warning {
961
- border-top: var(--jp-border-width) solid var(--jp-border-color2);
962
- border-left: 3px solid var(--jp-warn-color1);
963
- }
964
-
965
- .jp-ThemedContainer .Toastify__toast.jp-Notification-Toast-info {
966
- border-top: var(--jp-border-width) solid var(--jp-border-color2);
967
- border-left: 3px solid var(--jp-info-color1);
968
- }
969
-
970
- .jp-ThemedContainer .Toastify__toast.jp-Notification-Toast-success {
971
- border-top: var(--jp-border-width) solid var(--jp-border-color2);
972
- border-left: 3px solid var(--jp-success-color1);
973
- }
974
-
975
- .jp-ThemedContainer .Toastify__toast.jp-Notification-Toast-in-progress {
976
- border-top: var(--jp-border-width) solid var(--jp-border-color2);
977
- border-left: 3px solid var(--jp-brand-color1);
978
- }
979
- /* stylelint-enable selector-class-pattern */
980
-
981
939
  /* -----------------------------------------------------------------------------
982
940
  | Document search overlay
983
941
  |---------------------------------------------------------------------------- */