xtralab 0.13.0 → 0.13.1

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.
@@ -2,6 +2,7 @@ import { PageConfig } from '@jupyterlab/coreutils';
2
2
  import { homeIcon as rootIcon } from '@jupyterlab/ui-components';
3
3
  import { Widget } from '@lumino/widgets';
4
4
  const EDITOR_BREADCRUMBS_CLASS = 'jp-xtralab-EditorBreadcrumbs';
5
+ const TOOLBAR_SPACER_CLASS = 'jp-Toolbar-spacer';
5
6
  const BREADCRUMBS_CONTAINER_CLASS = 'jp-BreadCrumbs-container';
6
7
  const BREADCRUMBS_CONTENT_CLASS = 'jp-BreadCrumbs-content';
7
8
  const ROOT_CLASS = 'jp-BreadCrumbs-home';
@@ -25,6 +26,10 @@ export class EditorBreadcrumbs extends Widget {
25
26
  this._commands = options.commands;
26
27
  this._trans = options.trans;
27
28
  this.addClass(EDITOR_BREADCRUMBS_CLASS);
29
+ // The breadcrumbs stretch to fill the toolbar, so ReactiveToolbar's
30
+ // overflow math must count them as a spacer (2px) — measured at their
31
+ // stretched clientWidth they would evict every item into the popup.
32
+ this.addClass(TOOLBAR_SPACER_CLASS);
28
33
  this.node.setAttribute('aria-label', this._trans.__('Editor file path'));
29
34
  this._container = document.createElement('span');
30
35
  this._container.className = BREADCRUMBS_CONTAINER_CLASS;
@@ -20,6 +20,7 @@ export declare namespace CommandIDs {
20
20
  const download = "xtralab:download";
21
21
  const refresh = "xtralab:refresh";
22
22
  const collapseAll = "xtralab:collapse-all";
23
+ const toggleFileFilter = "xtralab:toggle-file-filter";
23
24
  const createNewDirectory = "xtralab:create-new-directory";
24
25
  const newLauncher = "xtralab:new-launcher";
25
26
  const revealPath = "xtralab:reveal-path";
@@ -52,12 +53,13 @@ export declare namespace ToolbarNames {
52
53
  const newDirectory = "new-directory";
53
54
  const refresh = "refresh";
54
55
  const collapseAll = "collapse-all";
56
+ const toggleFileFilter = "toggle-file-filter";
55
57
  }
56
58
  /**
57
59
  * Populate the file browser's toolbar with the buttons that mirror the
58
60
  * default JupyterLab file browser: a "+" launcher button, a new-folder
59
- * button, and a refresh button. Returns a list of button names so callers
60
- * can extend it or remove items if they need to.
61
+ * button, a refresh button, a collapse-all button, and the file filter
62
+ * toggle.
61
63
  */
62
64
  export declare function populateToolbar(opts: {
63
65
  app: JupyterFrontEnd;
@@ -1,7 +1,7 @@
1
1
  import { Clipboard, Dialog, InputDialog, showDialog, showErrorMessage } from '@jupyterlab/apputils';
2
2
  import { PageConfig, PathExt } from '@jupyterlab/coreutils';
3
3
  import { nullTranslator } from '@jupyterlab/translation';
4
- import { addIcon, closeIcon, collapseAllIcon, CommandToolbarButton, copyIcon, downloadIcon, editIcon, fileIcon, newFolderIcon, refreshIcon } from '@jupyterlab/ui-components';
4
+ import { addIcon, closeIcon, collapseAllIcon, CommandToolbarButton, copyIcon, downloadIcon, editIcon, fileIcon, filterIcon, newFolderIcon, refreshIcon } from '@jupyterlab/ui-components';
5
5
  import { toCanonicalPath, toServerPath } from './contents';
6
6
  import { FILE_BROWSER_ID } from './widget';
7
7
  /**
@@ -22,6 +22,7 @@ export var CommandIDs;
22
22
  CommandIDs.download = 'xtralab:download';
23
23
  CommandIDs.refresh = 'xtralab:refresh';
24
24
  CommandIDs.collapseAll = 'xtralab:collapse-all';
25
+ CommandIDs.toggleFileFilter = 'xtralab:toggle-file-filter';
25
26
  CommandIDs.createNewDirectory = 'xtralab:create-new-directory';
26
27
  CommandIDs.newLauncher = 'xtralab:new-launcher';
27
28
  CommandIDs.revealPath = 'xtralab:reveal-path';
@@ -393,6 +394,22 @@ export function registerCommands(opts) {
393
394
  browser.collapseAll();
394
395
  }
395
396
  });
397
+ commands.addCommand(CommandIDs.toggleFileFilter, {
398
+ label: trans.__('Toggle File Filter'),
399
+ caption: trans.__('Show or hide the file filter'),
400
+ icon: filterIcon.bindprops({ stylesheet: 'menuItem' }),
401
+ isToggled: () => browser.fileFilterVisible,
402
+ execute: () => {
403
+ browser.toggleFileFilter();
404
+ }
405
+ });
406
+ // Refresh the toggle's state however the filter is shown or hidden —
407
+ // the toolbar button, or the auto-show that kicks in when typing with
408
+ // the tree focused opens a search session.
409
+ const onFilterVisibleChanged = () => {
410
+ commands.notifyCommandChanged(CommandIDs.toggleFileFilter);
411
+ };
412
+ browser.fileFilterVisibleChanged.connect(onFilterVisibleChanged);
396
413
  // Public reveal seam. Other plugins (editor breadcrumbs, future
397
414
  // "reveal in tree" actions) call this command rather than depending
398
415
  // on the file browser widget directly. Activating the sidebar is part
@@ -492,6 +509,7 @@ export function registerCommands(opts) {
492
509
  var _a;
493
510
  app.contextMenu.opened.disconnect(updateOpenWithMenu);
494
511
  (_a = app.shell.currentChanged) === null || _a === void 0 ? void 0 : _a.disconnect(onCurrentChanged);
512
+ browser.fileFilterVisibleChanged.disconnect(onFilterVisibleChanged);
495
513
  paletteItem === null || paletteItem === void 0 ? void 0 : paletteItem.dispose();
496
514
  };
497
515
  }
@@ -506,12 +524,13 @@ export var ToolbarNames;
506
524
  ToolbarNames.newDirectory = 'new-directory';
507
525
  ToolbarNames.refresh = 'refresh';
508
526
  ToolbarNames.collapseAll = 'collapse-all';
527
+ ToolbarNames.toggleFileFilter = 'toggle-file-filter';
509
528
  })(ToolbarNames || (ToolbarNames = {}));
510
529
  /**
511
530
  * Populate the file browser's toolbar with the buttons that mirror the
512
531
  * default JupyterLab file browser: a "+" launcher button, a new-folder
513
- * button, and a refresh button. Returns a list of button names so callers
514
- * can extend it or remove items if they need to.
532
+ * button, a refresh button, a collapse-all button, and the file filter
533
+ * toggle.
515
534
  */
516
535
  export function populateToolbar(opts) {
517
536
  const { app, browser } = opts;
@@ -536,4 +555,9 @@ export function populateToolbar(opts) {
536
555
  id: CommandIDs.collapseAll,
537
556
  label: ''
538
557
  }));
558
+ browser.toolbar.addItem(ToolbarNames.toggleFileFilter, new CommandToolbarButton({
559
+ commands,
560
+ id: CommandIDs.toggleFileFilter,
561
+ label: ''
562
+ }));
539
563
  }
@@ -64,9 +64,20 @@ const GIT_REPO_PATH = '';
64
64
  * resolution cost just for one string.
65
65
  */
66
66
  const FILE_TREE_TAG = 'file-tree-container';
67
+ /**
68
+ * Injected into the tree's shadow root. The second rule hides the search
69
+ * box unless the host carries the visibility marker set by the filter
70
+ * bridge effect below — `@pierre/trees` always renders the box when
71
+ * `search` is enabled, and its stylesheet lives in the shadow root where
72
+ * outside CSS cannot reach.
73
+ */
67
74
  const FILE_TREE_UNSAFE_CSS = '[data-type="item"][data-item-selected="true"] ' +
68
75
  '[data-item-section="spacing-item"] {' +
69
76
  'border-left-color: transparent;' +
77
+ '}' +
78
+ ':host(:not([data-xtralab-filter-visible])) ' +
79
+ '[data-file-tree-search-container] {' +
80
+ 'display: none;' +
70
81
  '}';
71
82
  /**
72
83
  * Renders a `@pierre/trees` file tree backed by the Jupyter contents API.
@@ -652,6 +663,17 @@ export function FileBrowserComponent(props) {
652
663
  };
653
664
  contentsManager.fileChanged.connect(onContentsFileChanged);
654
665
  const unsubscribe = model.subscribe(() => {
666
+ // Search-driven expansion must not trigger fetches: the tree
667
+ // auto-expands every directory whose path matches the query, and
668
+ // treating those as user expansions would recursively fetch every
669
+ // matching subtree — a single common letter can walk the whole
670
+ // workspace, node_modules included. Closing the search restores
671
+ // the pre-search expansion state, and a directory clicked in the
672
+ // results is toggled after the session closes, so real expansions
673
+ // are still fetched the moment the session ends.
674
+ if (model.isSearchOpen()) {
675
+ return;
676
+ }
655
677
  knownDirs.forEach((state, canonicalPath) => {
656
678
  if (state !== 'unloaded') {
657
679
  return;
@@ -744,6 +766,49 @@ export function FileBrowserComponent(props) {
744
766
  unsubscribe();
745
767
  };
746
768
  }, [model, widget]);
769
+ // Bridge the filter-box visibility between the widget and the tree. The
770
+ // widget owns the flag; applying it stamps the marker attribute the
771
+ // unsafeCSS rule keys on and opens or closes the model's search session
772
+ // (opening focuses the input, closing clears any active filter). The
773
+ // model subscription covers the reverse direction: typing a printable
774
+ // character while the tree has focus opens a session on the tree's own
775
+ // initiative, and the box must surface for that session to be usable.
776
+ React.useEffect(() => {
777
+ if (widget === undefined) {
778
+ return;
779
+ }
780
+ const apply = (visible) => {
781
+ const host = model.getFileTreeContainer();
782
+ if (host !== undefined) {
783
+ if (visible) {
784
+ host.dataset.xtralabFilterVisible = 'true';
785
+ }
786
+ else {
787
+ delete host.dataset.xtralabFilterVisible;
788
+ }
789
+ }
790
+ if (visible && !model.isSearchOpen()) {
791
+ model.openSearch();
792
+ }
793
+ else if (!visible && model.isSearchOpen()) {
794
+ model.closeSearch();
795
+ }
796
+ };
797
+ apply(widget.fileFilterVisible);
798
+ const visibleSlot = (sender, visible) => {
799
+ apply(visible);
800
+ };
801
+ widget.fileFilterVisibleChanged.connect(visibleSlot);
802
+ const unsubscribe = model.subscribe(() => {
803
+ if (model.isSearchOpen() && !widget.fileFilterVisible) {
804
+ widget.setFileFilterVisible(true);
805
+ }
806
+ });
807
+ return () => {
808
+ widget.fileFilterVisibleChanged.disconnect(visibleSlot);
809
+ unsubscribe();
810
+ };
811
+ }, [model, widget]);
747
812
  const wrapperRef = React.useRef(null);
748
813
  React.useEffect(() => {
749
814
  const wrapper = wrapperRef.current;
@@ -71,6 +71,19 @@ export interface IXtralabFileBrowser {
71
71
  * directories, calling `.collapse()` on each expanded one.
72
72
  */
73
73
  readonly collapseAllRequested: ISignal<IXtralabFileBrowser, void>;
74
+ /**
75
+ * Whether the filter box above the tree is shown. Mirrors the default
76
+ * file browser's `showFileFilter`: hidden on startup and flipped by the
77
+ * toolbar toggle. Also forced to `true` when the tree opens a search
78
+ * session on its own — typing a letter while the tree has focus.
79
+ */
80
+ readonly fileFilterVisible: boolean;
81
+ /**
82
+ * Emits when {@link fileFilterVisible} changes. The React component
83
+ * listens to show or hide the filter box; the toggle command listens
84
+ * to refresh its toggled state.
85
+ */
86
+ readonly fileFilterVisibleChanged: ISignal<IXtralabFileBrowser, boolean>;
74
87
  /**
75
88
  * Trigger a refresh of every loaded directory in the tree.
76
89
  */
@@ -98,6 +111,14 @@ export interface IXtralabFileBrowser {
98
111
  * Ask the tree to collapse every currently expanded folder.
99
112
  */
100
113
  collapseAll(): void;
114
+ /**
115
+ * Show or hide the filter box above the tree.
116
+ */
117
+ setFileFilterVisible(visible: boolean): void;
118
+ /**
119
+ * Toggle the filter box above the tree.
120
+ */
121
+ toggleFileFilter(): void;
101
122
  }
102
123
  /**
103
124
  * Lumino widget that hosts the React-based `@pierre/trees` file browser.
@@ -115,6 +136,8 @@ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBr
115
136
  get revealRequested(): ISignal<this, string>;
116
137
  get rootRequested(): ISignal<this, void>;
117
138
  get collapseAllRequested(): ISignal<this, void>;
139
+ get fileFilterVisible(): boolean;
140
+ get fileFilterVisibleChanged(): ISignal<this, boolean>;
118
141
  /**
119
142
  * Update the cached selection. Called from the React tree when the
120
143
  * underlying `@pierre/trees` model emits a selection change.
@@ -125,6 +148,8 @@ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBr
125
148
  reveal(canonicalPath: string): void;
126
149
  scrollToRoot(): void;
127
150
  collapseAll(): void;
151
+ setFileFilterVisible(visible: boolean): void;
152
+ toggleFileFilter(): void;
128
153
  private _contentsManager;
129
154
  private _docManager;
130
155
  private _onOpenFile;
@@ -135,6 +160,8 @@ export declare class XtralabFileBrowser extends Widget implements IXtralabFileBr
135
160
  private _revealRequested;
136
161
  private _rootRequested;
137
162
  private _collapseAllRequested;
163
+ private _fileFilterVisible;
164
+ private _fileFilterVisibleChanged;
138
165
  private _toolbar;
139
166
  private _content;
140
167
  }
@@ -29,6 +29,8 @@ export class XtralabFileBrowser extends Widget {
29
29
  this._revealRequested = new Signal(this);
30
30
  this._rootRequested = new Signal(this);
31
31
  this._collapseAllRequested = new Signal(this);
32
+ this._fileFilterVisible = false;
33
+ this._fileFilterVisibleChanged = new Signal(this);
32
34
  const layout = (this.layout = new PanelLayout());
33
35
  this._contentsManager = options.contentsManager;
34
36
  this._docManager = options.docManager;
@@ -77,6 +79,12 @@ export class XtralabFileBrowser extends Widget {
77
79
  get collapseAllRequested() {
78
80
  return this._collapseAllRequested;
79
81
  }
82
+ get fileFilterVisible() {
83
+ return this._fileFilterVisible;
84
+ }
85
+ get fileFilterVisibleChanged() {
86
+ return this._fileFilterVisibleChanged;
87
+ }
80
88
  /**
81
89
  * Update the cached selection. Called from the React tree when the
82
90
  * underlying `@pierre/trees` model emits a selection change.
@@ -103,6 +111,16 @@ export class XtralabFileBrowser extends Widget {
103
111
  collapseAll() {
104
112
  this._collapseAllRequested.emit();
105
113
  }
114
+ setFileFilterVisible(visible) {
115
+ if (this._fileFilterVisible === visible) {
116
+ return;
117
+ }
118
+ this._fileFilterVisible = visible;
119
+ this._fileFilterVisibleChanged.emit(visible);
120
+ }
121
+ toggleFileFilter() {
122
+ this.setFileFilterVisible(!this._fileFilterVisible);
123
+ }
106
124
  }
107
125
  /**
108
126
  * The React-rendered region of the file browser. Lives inside the host
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtralab",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
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",
@@ -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, and a refresh button. Returns a list of button names so callers
614
- * can extend it or remove items if they need to.
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
  }
@@ -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/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
  |---------------------------------------------------------------------------- */