xtralab 0.2.0 → 0.4.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.
Files changed (39) hide show
  1. package/README.md +126 -125
  2. package/lib/editorBreadcrumbs/index.d.ts +11 -0
  3. package/lib/editorBreadcrumbs/index.js +41 -0
  4. package/lib/editorBreadcrumbs/widget.d.ts +34 -0
  5. package/lib/editorBreadcrumbs/widget.js +131 -0
  6. package/lib/editorIndent/index.d.ts +17 -0
  7. package/lib/editorIndent/index.js +142 -0
  8. package/lib/fileBrowser/commands.d.ts +5 -2
  9. package/lib/fileBrowser/commands.js +42 -3
  10. package/lib/fileBrowser/fileBrowser.js +394 -11
  11. package/lib/fileBrowser/gitStatus.d.ts +13 -0
  12. package/lib/fileBrowser/gitStatus.js +60 -0
  13. package/lib/fileBrowser/icons.d.ts +4 -4
  14. package/lib/fileBrowser/icons.js +4 -4
  15. package/lib/fileBrowser/widget.d.ts +50 -2
  16. package/lib/fileBrowser/widget.js +26 -2
  17. package/lib/git/icons.d.ts +1 -1
  18. package/lib/git/icons.js +1 -1
  19. package/lib/git/panelComponent.js +28 -11
  20. package/lib/index.js +4 -0
  21. package/lib/launcher/dashboard.js +78 -16
  22. package/package.json +22 -2
  23. package/src/editorBreadcrumbs/index.ts +70 -0
  24. package/src/editorBreadcrumbs/widget.ts +158 -0
  25. package/src/editorIndent/index.ts +177 -0
  26. package/src/fileBrowser/commands.ts +45 -3
  27. package/src/fileBrowser/fileBrowser.tsx +428 -12
  28. package/src/fileBrowser/gitStatus.ts +66 -0
  29. package/src/fileBrowser/icons.ts +4 -4
  30. package/src/fileBrowser/widget.tsx +80 -3
  31. package/src/git/icons.ts +1 -1
  32. package/src/git/panelComponent.tsx +29 -15
  33. package/src/index.ts +4 -0
  34. package/src/launcher/dashboard.tsx +135 -16
  35. package/style/editorBreadcrumbs.css +86 -0
  36. package/style/index.css +2 -0
  37. package/style/index.js +1 -0
  38. package/style/launcher.css +0 -5
  39. package/style/tabs.css +66 -0
@@ -12,7 +12,7 @@ import { xtralabFileBrowserIcon } from './icons';
12
12
  export const FILE_BROWSER_ID = 'xtralab:file-browser';
13
13
 
14
14
  /**
15
- * The CSS class added to the Xtralab file browser widget. The selectors that
15
+ * The CSS class added to the xtralab file browser widget. The selectors that
16
16
  * bind application context menu items hang off this class, so it must remain
17
17
  * specific enough not to clash with other browsers (such as the default
18
18
  * `jp-FileBrowser`).
@@ -29,7 +29,7 @@ export interface IXtralabFileBrowserOptions {
29
29
  }
30
30
 
31
31
  /**
32
- * Public API of the Xtralab file browser used by command handlers and other
32
+ * Public API of the xtralab file browser used by command handlers and other
33
33
  * collaborators. Kept narrow on purpose so the React component stays free to
34
34
  * evolve the underlying tree integration.
35
35
  */
@@ -74,6 +74,31 @@ export interface IXtralabFileBrowser {
74
74
  */
75
75
  readonly pathAdded: ISignal<IXtralabFileBrowser, string>;
76
76
 
77
+ /**
78
+ * Emits when an external caller asks the tree to scroll to and select
79
+ * the given canonical path. The React component listens, lazily loads
80
+ * any unloaded ancestor directories, expands them, and selects the
81
+ * target. Used by the editor breadcrumbs to jump back to a file or
82
+ * folder shown in the breadcrumb trail.
83
+ */
84
+ readonly revealRequested: ISignal<IXtralabFileBrowser, string>;
85
+
86
+ /**
87
+ * Emits when an external caller asks the tree to return to the
88
+ * workspace root: clear any current selection and scroll back to the
89
+ * first row. Distinct from {@link revealRequested} because the
90
+ * workspace root has no tree row of its own — it cannot be reached
91
+ * by passing a path.
92
+ */
93
+ readonly rootRequested: ISignal<IXtralabFileBrowser, void>;
94
+
95
+ /**
96
+ * Emits when an external caller asks the tree to collapse every
97
+ * expanded folder. The React component listens and walks the loaded
98
+ * directories, calling `.collapse()` on each expanded one.
99
+ */
100
+ readonly collapseAllRequested: ISignal<IXtralabFileBrowser, void>;
101
+
77
102
  /** Trigger a refresh of every loaded directory in the tree. */
78
103
  refresh(): void;
79
104
 
@@ -83,6 +108,26 @@ export interface IXtralabFileBrowser {
83
108
  * convention: directories carry a trailing slash, files do not.
84
109
  */
85
110
  notifyPathAdded(canonicalPath: string): void;
111
+
112
+ /**
113
+ * Ask the tree to reveal {@link canonicalPath}: load and expand any
114
+ * unloaded ancestor directories, then select and scroll the target
115
+ * into view. `canonicalPath` follows the `@pierre/trees` convention
116
+ * (directories carry a trailing slash, files do not). Must be a
117
+ * non-empty path — use {@link scrollToRoot} for the root gesture.
118
+ */
119
+ reveal(canonicalPath: string): void;
120
+
121
+ /**
122
+ * Ask the tree to return to the workspace root: clear any current
123
+ * selection and scroll back to the top of the tree.
124
+ */
125
+ scrollToRoot(): void;
126
+
127
+ /**
128
+ * Ask the tree to collapse every currently expanded folder.
129
+ */
130
+ collapseAll(): void;
86
131
  }
87
132
 
88
133
  /**
@@ -100,7 +145,7 @@ export class XtralabFileBrowser extends Widget implements IXtralabFileBrowser {
100
145
  this._onOpenFile = options.onOpenFile;
101
146
  this.id = FILE_BROWSER_ID;
102
147
  this.title.icon = xtralabFileBrowserIcon;
103
- this.title.caption = 'Xtralab File Browser';
148
+ this.title.caption = 'xtralab File Browser';
104
149
  this.addClass(FILE_BROWSER_CSS_CLASS);
105
150
 
106
151
  this._toolbar = new Toolbar();
@@ -142,6 +187,18 @@ export class XtralabFileBrowser extends Widget implements IXtralabFileBrowser {
142
187
  return this._pathAdded;
143
188
  }
144
189
 
190
+ get revealRequested(): ISignal<this, string> {
191
+ return this._revealRequested;
192
+ }
193
+
194
+ get rootRequested(): ISignal<this, void> {
195
+ return this._rootRequested;
196
+ }
197
+
198
+ get collapseAllRequested(): ISignal<this, void> {
199
+ return this._collapseAllRequested;
200
+ }
201
+
145
202
  /**
146
203
  * Update the cached selection. Called from the React tree when the
147
204
  * underlying `@pierre/trees` model emits a selection change.
@@ -159,6 +216,23 @@ export class XtralabFileBrowser extends Widget implements IXtralabFileBrowser {
159
216
  this._pathAdded.emit(canonicalPath);
160
217
  }
161
218
 
219
+ reveal(canonicalPath: string): void {
220
+ if (canonicalPath.length === 0) {
221
+ throw new Error(
222
+ 'XtralabFileBrowser.reveal requires a non-empty canonical path; use scrollToRoot for the workspace root.'
223
+ );
224
+ }
225
+ this._revealRequested.emit(canonicalPath);
226
+ }
227
+
228
+ scrollToRoot(): void {
229
+ this._rootRequested.emit();
230
+ }
231
+
232
+ collapseAll(): void {
233
+ this._collapseAllRequested.emit();
234
+ }
235
+
162
236
  private _contentsManager: Contents.IManager;
163
237
  private _docManager: IDocumentManager;
164
238
  private _onOpenFile: ((serverPath: string) => void) | undefined;
@@ -166,6 +240,9 @@ export class XtralabFileBrowser extends Widget implements IXtralabFileBrowser {
166
240
  private _selectionChanged = new Signal<this, readonly string[]>(this);
167
241
  private _refreshRequested = new Signal<this, void>(this);
168
242
  private _pathAdded = new Signal<this, string>(this);
243
+ private _revealRequested = new Signal<this, string>(this);
244
+ private _rootRequested = new Signal<this, void>(this);
245
+ private _collapseAllRequested = new Signal<this, void>(this);
169
246
  private _toolbar: Toolbar;
170
247
  private _content: XtralabFileTreeContent;
171
248
  }
package/src/git/icons.ts CHANGED
@@ -6,7 +6,7 @@ import { LabIcon } from '@jupyterlab/ui-components';
6
6
  * recoloring and inverse-color sidebar painting automatically.
7
7
  *
8
8
  * Source: `git-branch` from SVG Repo (https://www.svgrepo.com/svg/377352/git-branch),
9
- * part of the same Pixelarticons family as the Xtralab file browser's `file-tree`
9
+ * part of the same Pixelarticons family as the xtralab file browser's `file-tree`
10
10
  * icon, so the two sidebar tab glyphs read as a matched set. The original
11
11
  * `fill="#000000"` is replaced with `fill="currentColor"` and the root element
12
12
  * is given the `jp-icon3` class so the icon picks up JupyterLab's theme color
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
 
3
+ import { Poll } from '@lumino/polling';
3
4
  import { ISignal } from '@lumino/signaling';
4
5
 
5
6
  import {
@@ -49,6 +50,14 @@ interface IPanelState {
49
50
  */
50
51
  const STATUS_POLL_INTERVAL_MS = 5000;
51
52
 
53
+ /**
54
+ * Upper bound on the poll's exponential backoff when status requests fail
55
+ * repeatedly. Matches the default JupyterLab file browser's `max`: long
56
+ * enough that a server outage stops hammering the API, short enough that
57
+ * a transient failure heals on its own.
58
+ */
59
+ const STATUS_POLL_MAX_MS = 300_000;
60
+
52
61
  /**
53
62
  * Top-level React component rendered inside the {@link GitPanel} widget.
54
63
  * Owns the polling + refresh wiring around the `/git/status` endpoint and
@@ -109,27 +118,32 @@ export function GitPanelComponent(props: {
109
118
  }
110
119
  }, [repoPath]);
111
120
 
112
- // Initial load + polling. `refresh` is stable for a given `repoPath`, so
113
- // the polling interval is only re-created when the repo path changes.
114
- React.useEffect(() => {
115
- void refresh();
116
- const interval = window.setInterval(
117
- () => void refresh(),
118
- STATUS_POLL_INTERVAL_MS
119
- );
120
- return () => {
121
- window.clearInterval(interval);
122
- };
123
- }, [refresh]);
124
-
125
- // External refresh triggers (toolbar button, plugin-level events).
121
+ // Initial load + polling. Driven by a Lumino `Poll` so the panel stops
122
+ // hitting the git endpoint while the JupyterLab tab is hidden, and so
123
+ // repeated failures back off exponentially instead of spinning at 5s.
124
+ // External refresh triggers (toolbar button, plugin-level events) call
125
+ // `poll.refresh()` to schedule an immediate tick instead of running
126
+ // `refresh()` directly — that keeps the panel from issuing two parallel
127
+ // status requests when an automatic tick happens to land at the same
128
+ // time as an external trigger.
126
129
  React.useEffect(() => {
130
+ const poll = new Poll({
131
+ name: '@xtralab/git:panelStatus',
132
+ factory: () => refresh(),
133
+ frequency: {
134
+ interval: STATUS_POLL_INTERVAL_MS,
135
+ backoff: true,
136
+ max: STATUS_POLL_MAX_MS
137
+ },
138
+ standby: 'when-hidden'
139
+ });
127
140
  const handler = (): void => {
128
- void refresh();
141
+ void poll.refresh();
129
142
  };
130
143
  refreshSignal.connect(handler);
131
144
  return () => {
132
145
  refreshSignal.disconnect(handler);
146
+ poll.dispose();
133
147
  };
134
148
  }, [refresh, refreshSignal]);
135
149
 
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
2
 
3
3
  import fileBrowserPlugin from './fileBrowser';
4
+ import editorBreadcrumbsPlugin from './editorBreadcrumbs';
5
+ import editorIndentPlugin from './editorIndent';
4
6
  import gitPlugin from './git';
5
7
  import launcherPlugin from './launcher';
6
8
  import menusPlugin from './menus';
@@ -15,6 +17,8 @@ import statusBarPlugin from './statusBar';
15
17
  * available end up running.
16
18
  */
17
19
  const plugins: JupyterFrontEndPlugin<unknown>[] = [
20
+ editorBreadcrumbsPlugin,
21
+ editorIndentPlugin,
18
22
  fileBrowserPlugin,
19
23
  gitPlugin,
20
24
  launcherPlugin,
@@ -2,7 +2,15 @@ import * as React from 'react';
2
2
 
3
3
  import type { CommandRegistry } from '@lumino/commands';
4
4
  import type { ReadonlyPartialJSONObject } from '@lumino/coreutils';
5
- import { LabIcon, ReactWidget, refreshIcon } from '@jupyterlab/ui-components';
5
+ import { Poll } from '@lumino/polling';
6
+ import {
7
+ LabIcon,
8
+ ReactWidget,
9
+ consoleIcon,
10
+ notebookIcon,
11
+ refreshIcon,
12
+ terminalIcon
13
+ } from '@jupyterlab/ui-components';
6
14
  import type { Widget } from '@lumino/widgets';
7
15
 
8
16
  import { expandStatusFiles, status } from '../git/api';
@@ -83,6 +91,13 @@ interface IGitState {
83
91
  */
84
92
  const CHANGES_POLL_INTERVAL_MS = 8000;
85
93
 
94
+ /**
95
+ * Upper bound on the exponential backoff when the status request fails
96
+ * repeatedly. Matches the rest of the plugin's polls so behavior is
97
+ * consistent across the dashboard, the git panel, and the file tree.
98
+ */
99
+ const CHANGES_POLL_MAX_MS = 300_000;
100
+
86
101
  function LauncherDashboardComponent(
87
102
  props: ILauncherDashboardOptions
88
103
  ): React.ReactElement {
@@ -107,12 +122,23 @@ function LauncherDashboardComponent(
107
122
  }
108
123
  }, [repoPath]);
109
124
 
125
+ // Driven by a Lumino `Poll` so the dashboard stops hitting the git
126
+ // endpoint while the JupyterLab tab is hidden, and so repeated
127
+ // failures back off exponentially instead of pegging the 8s cadence.
110
128
  React.useEffect(() => {
111
- void refresh();
112
- const handle = window.setInterval(() => {
113
- void refresh();
114
- }, CHANGES_POLL_INTERVAL_MS);
115
- return () => window.clearInterval(handle);
129
+ const poll = new Poll({
130
+ name: '@xtralab/launcher:gitChanges',
131
+ factory: () => refresh(),
132
+ frequency: {
133
+ interval: CHANGES_POLL_INTERVAL_MS,
134
+ backoff: true,
135
+ max: CHANGES_POLL_MAX_MS
136
+ },
137
+ standby: 'when-hidden'
138
+ });
139
+ return () => {
140
+ poll.dispose();
141
+ };
116
142
  }, [refresh]);
117
143
 
118
144
  const launch = React.useCallback(
@@ -133,6 +159,36 @@ function LauncherDashboardComponent(
133
159
  [commands, cwd, prompt, onAgentLaunch]
134
160
  );
135
161
 
162
+ const launchTerminal = React.useCallback(async () => {
163
+ const result = (await commands.execute('terminal:create-new', {
164
+ cwd
165
+ })) as Widget | undefined;
166
+ if (result) {
167
+ onAgentLaunch(result);
168
+ }
169
+ }, [commands, cwd, onAgentLaunch]);
170
+
171
+ const launchNotebook = React.useCallback(async () => {
172
+ // No `kernelName` — let the notebook extension fall back to the default
173
+ // kernel or prompt the user, matching the stock launcher's notebook
174
+ // tile behavior.
175
+ const result = (await commands.execute('notebook:create-new', {
176
+ cwd
177
+ })) as Widget | undefined;
178
+ if (result) {
179
+ onAgentLaunch(result);
180
+ }
181
+ }, [commands, cwd, onAgentLaunch]);
182
+
183
+ const launchConsole = React.useCallback(async () => {
184
+ const result = (await commands.execute('console:create', {
185
+ cwd
186
+ })) as Widget | undefined;
187
+ if (result) {
188
+ onAgentLaunch(result);
189
+ }
190
+ }, [commands, cwd, onAgentLaunch]);
191
+
136
192
  const openDiff = React.useCallback(
137
193
  (change: IFileChange) => {
138
194
  const args: GitCommandArguments.IOpenDiff = { repoPath, change };
@@ -152,6 +208,11 @@ function LauncherDashboardComponent(
152
208
  onPromptChange={setPrompt}
153
209
  onLaunch={launch}
154
210
  />
211
+ <OpenSection
212
+ onLaunchTerminal={launchTerminal}
213
+ onLaunchNotebook={launchNotebook}
214
+ onLaunchConsole={launchConsole}
215
+ />
155
216
  <ChangesSection
156
217
  git={git}
157
218
  onOpen={openDiff}
@@ -278,10 +339,9 @@ function AgentSection(props: {
278
339
  const { agents, prompt, onPromptChange, onLaunch } = props;
279
340
  const trimmed = prompt.trim();
280
341
  const promptActive = trimmed.length > 0;
281
- // The "primary" agent is the first prompt-capable one in the rendered
282
- // order. We mark it visually so the keyboard shortcut target isn't a
283
- // mystery, and reuse the same agent when Cmd/Ctrl+Enter fires from the
284
- // textarea — picking by the same rule keeps the two behaviors in sync.
342
+ // The first prompt-capable agent is what Cmd/Ctrl+Enter fires; the hint
343
+ // text below the textarea names it so the keyboard shortcut target
344
+ // isn't a mystery.
285
345
  const primaryAgent = agents.find(agent => agent.promptArgs !== undefined);
286
346
  const hintId = 'jp-xtralab-Launcher-agent-hint';
287
347
 
@@ -320,19 +380,14 @@ function AgentSection(props: {
320
380
  {agents.map(agent => {
321
381
  const supportsPrompt = agent.promptArgs !== undefined;
322
382
  const disabled = promptActive && !supportsPrompt;
323
- const isPrimary = agent === primaryAgent;
324
383
  const tooltip = disabled
325
384
  ? `${agent.label} doesn't accept an initial prompt — clear the prompt to launch.`
326
385
  : agent.caption;
327
- const classes = ['jp-xtralab-Launcher-agent'];
328
- if (isPrimary) {
329
- classes.push('jp-xtralab-Launcher-agent-primary');
330
- }
331
386
  return (
332
387
  <button
333
388
  key={agent.id}
334
389
  type="button"
335
- className={classes.join(' ')}
390
+ className="jp-xtralab-Launcher-agent"
336
391
  title={tooltip}
337
392
  aria-label={agent.label}
338
393
  aria-describedby={hintId}
@@ -354,3 +409,67 @@ function AgentSection(props: {
354
409
  </section>
355
410
  );
356
411
  }
412
+
413
+ /**
414
+ * The non-agent launch tiles — a plain terminal, a new notebook, and a
415
+ * new console. They sit in a separate section below the agent grid so
416
+ * they don't compete with the AI agents visually, and they're always
417
+ * available regardless of the agent prompt textarea (which doesn't
418
+ * apply to them).
419
+ */
420
+ function OpenSection(props: {
421
+ onLaunchTerminal: () => void;
422
+ onLaunchNotebook: () => void;
423
+ onLaunchConsole: () => void;
424
+ }): React.ReactElement {
425
+ const { onLaunchTerminal, onLaunchNotebook, onLaunchConsole } = props;
426
+ return (
427
+ <section className="jp-xtralab-Launcher-section">
428
+ <h2 className="jp-xtralab-Launcher-section-title">Open</h2>
429
+ <div className="jp-xtralab-Launcher-agents">
430
+ <button
431
+ type="button"
432
+ className="jp-xtralab-Launcher-agent"
433
+ title="Open a new terminal."
434
+ aria-label="Open a new terminal"
435
+ onClick={onLaunchTerminal}
436
+ >
437
+ <LabIcon.resolveReact
438
+ icon={terminalIcon}
439
+ tag="span"
440
+ className="jp-xtralab-Launcher-agent-icon"
441
+ />
442
+ <span className="jp-xtralab-Launcher-agent-label">Terminal</span>
443
+ </button>
444
+ <button
445
+ type="button"
446
+ className="jp-xtralab-Launcher-agent"
447
+ title="Create a new notebook."
448
+ aria-label="Create a new notebook"
449
+ onClick={onLaunchNotebook}
450
+ >
451
+ <LabIcon.resolveReact
452
+ icon={notebookIcon}
453
+ tag="span"
454
+ className="jp-xtralab-Launcher-agent-icon"
455
+ />
456
+ <span className="jp-xtralab-Launcher-agent-label">Notebook</span>
457
+ </button>
458
+ <button
459
+ type="button"
460
+ className="jp-xtralab-Launcher-agent"
461
+ title="Create a new console."
462
+ aria-label="Create a new console"
463
+ onClick={onLaunchConsole}
464
+ >
465
+ <LabIcon.resolveReact
466
+ icon={consoleIcon}
467
+ tag="span"
468
+ className="jp-xtralab-Launcher-agent-icon"
469
+ />
470
+ <span className="jp-xtralab-Launcher-agent-label">Console</span>
471
+ </button>
472
+ </div>
473
+ </section>
474
+ );
475
+ }
@@ -0,0 +1,86 @@
1
+ .jp-xtralab-EditorBreadcrumbs.jp-Toolbar-item {
2
+ flex: 1 1 auto;
3
+ display: flex;
4
+ align-items: center;
5
+ align-self: center;
6
+ box-sizing: border-box;
7
+ min-width: 0;
8
+ height: 28px;
9
+ min-height: 28px;
10
+ max-height: 28px;
11
+ margin: 0;
12
+ padding: 0 10px;
13
+ color: var(--jp-ui-font-color2);
14
+ font-family: var(--jp-ui-font-family);
15
+ font-size: var(--jp-ui-font-size1);
16
+ overflow: hidden;
17
+ }
18
+
19
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-container {
20
+ display: flex;
21
+ align-items: center;
22
+ min-width: 0;
23
+ max-width: 100%;
24
+ height: 100%;
25
+ }
26
+
27
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-content {
28
+ display: flex;
29
+ align-items: center;
30
+ min-width: 0;
31
+ max-width: 100%;
32
+ overflow: hidden;
33
+ white-space: nowrap;
34
+ }
35
+
36
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-home {
37
+ display: inline-flex;
38
+ align-items: center;
39
+ flex: 0 0 auto;
40
+ margin-right: 2px;
41
+ padding: 2px;
42
+ border-radius: var(--jp-border-radius);
43
+ cursor: pointer;
44
+ }
45
+
46
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-home:hover {
47
+ background-color: var(--jp-layout-color2);
48
+ }
49
+
50
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-home:focus-visible {
51
+ outline: 2px solid var(--jp-brand-color1);
52
+ }
53
+
54
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-item {
55
+ flex: 0 1 auto;
56
+ min-width: 0;
57
+ margin: 0 2px;
58
+ overflow: hidden;
59
+ padding: 2px;
60
+ border-radius: var(--jp-border-radius);
61
+ cursor: pointer;
62
+ line-height: 18px;
63
+ text-overflow: ellipsis;
64
+ }
65
+
66
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-item,
67
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-separator {
68
+ vertical-align: middle;
69
+ }
70
+
71
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-item:hover {
72
+ background-color: var(--jp-layout-color2);
73
+ }
74
+
75
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-item:focus-visible {
76
+ outline: 2px solid var(--jp-brand-color1);
77
+ }
78
+
79
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-item.jp-mod-current {
80
+ color: var(--jp-ui-font-color1);
81
+ }
82
+
83
+ .jp-xtralab-EditorBreadcrumbs .jp-BreadCrumbs-separator {
84
+ flex: 0 0 auto;
85
+ line-height: 18px;
86
+ }
package/style/index.css CHANGED
@@ -1,4 +1,6 @@
1
1
  @import url('base.css');
2
+ @import url('editorBreadcrumbs.css');
2
3
  @import url('git.css');
3
4
  @import url('launcher.css');
4
5
  @import url('statusBar.css');
6
+ @import url('tabs.css');
package/style/index.js CHANGED
@@ -2,3 +2,4 @@ import './base.css';
2
2
  import './git.css';
3
3
  import './launcher.css';
4
4
  import './statusBar.css';
5
+ import './tabs.css';
@@ -295,11 +295,6 @@
295
295
  cursor: not-allowed;
296
296
  }
297
297
 
298
- .jp-xtralab-Launcher-agent-primary {
299
- border-color: var(--jp-brand-color2);
300
- box-shadow: 0 0 0 1px var(--jp-brand-color2);
301
- }
302
-
303
298
  .jp-xtralab-Launcher-agent:hover:not(:disabled) {
304
299
  background-color: var(--jp-layout-color2);
305
300
  border-color: var(--jp-border-color1);
package/style/tabs.css ADDED
@@ -0,0 +1,66 @@
1
+ /*
2
+ * Adaptive (VS Code-style) document tab widths. Tabs size to their filename
3
+ * instead of being pinned to --jp-private-horizontal-tab-width (216px), and
4
+ * the horizontal tab strip becomes scrollable when tabs overflow.
5
+ *
6
+ * Mostly mirrors jupyterlab/jupyterlab#18796, with one deliberate divergence:
7
+ * .lm-TabBar-content uses `flex: 0 1 auto` (not `1 1 auto`) so the +/add
8
+ * button sits next to the last tab instead of being pushed to the right
9
+ * edge of the bar. Overflow scrolling still works because `flex-shrink: 1`
10
+ * plus `min-width: 0` lets content shrink when tabs exceed the bar width.
11
+ */
12
+
13
+ .lm-DockPanel-tabBar .lm-TabBar-tab,
14
+ .lm-TabPanel-tabBar .lm-TabBar-tab {
15
+ flex: 0 0 auto;
16
+ padding: 0 8px;
17
+ cursor: pointer;
18
+ }
19
+
20
+ .lm-DockPanel-tabBar .lm-TabBar-addButton,
21
+ .lm-TabPanel-tabBar .lm-TabBar-addButton {
22
+ cursor: pointer;
23
+ }
24
+
25
+ .lm-DockPanel-tabBar .lm-TabBar-tab {
26
+ width: auto;
27
+ }
28
+
29
+ .lm-DockPanel-tabBar .lm-TabBar-content {
30
+ flex: 0 1 auto;
31
+ min-width: 0;
32
+ }
33
+
34
+ .lm-DockPanel-tabBar[data-orientation='horizontal'] {
35
+ overflow: hidden;
36
+ }
37
+
38
+ .lm-DockPanel-tabBar[data-orientation='horizontal'] > .lm-TabBar-content {
39
+ overflow: auto hidden;
40
+ position: relative;
41
+ z-index: 3;
42
+ }
43
+
44
+ .lm-TabBar-content::-webkit-scrollbar {
45
+ height: 2px;
46
+ background: var(--jp-layout-color1);
47
+ }
48
+
49
+ .lm-TabBar-content::-webkit-scrollbar-button {
50
+ display: none;
51
+ width: 0;
52
+ height: 0;
53
+ }
54
+
55
+ .lm-TabBar-content::-webkit-scrollbar-track {
56
+ background: var(--jp-layout-color1);
57
+ }
58
+
59
+ .lm-TabBar-content::-webkit-scrollbar-thumb {
60
+ background: rgb(120 120 120 / 40%);
61
+ border-radius: 999px;
62
+ }
63
+
64
+ .lm-TabBar-content:hover::-webkit-scrollbar-thumb {
65
+ background: rgb(160 160 160 / 60%);
66
+ }