jupyterlab_colourful_tab_extension 1.0.10

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/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, Stellars Henson
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # jupyterlab_colourful_tab_extension
2
+
3
+ [![GitHub Actions](https://github.com/stellarshenson/jupyterlab_colourful_tab_extension/actions/workflows/build.yml/badge.svg)](https://github.com/stellarshenson/jupyterlab_colourful_tab_extension/actions/workflows/build.yml)
4
+ [![npm version](https://img.shields.io/npm/v/jupyterlab_colourful_tab_extension.svg)](https://www.npmjs.com/package/jupyterlab_colourful_tab_extension)
5
+ [![PyPI version](https://img.shields.io/pypi/v/jupyterlab-colourful-tab-extension.svg)](https://pypi.org/project/jupyterlab-colourful-tab-extension/)
6
+ [![Total PyPI downloads](https://static.pepy.tech/badge/jupyterlab-colourful-tab-extension)](https://pepy.tech/project/jupyterlab-colourful-tab-extension)
7
+ [![JupyterLab 4](https://img.shields.io/badge/JupyterLab-4-orange.svg)](https://jupyterlab.readthedocs.io/en/stable/)
8
+ [![Brought To You By KOLOMOLO](https://img.shields.io/badge/Brought%20To%20You%20By-KOLOMOLO-00ffff?style=flat)](https://kolomolo.com)
9
+ [![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-blue?style=flat)](https://www.paypal.com/donate/?hosted_button_id=B4KPBJDLLXTSA)
10
+
11
+ A JupyterLab extension that applies pastel colours to tabs for visual identification when many tabs are open.
12
+
13
+ Coloured tabs make it easy to identify different notebooks and files at a glance.
14
+
15
+ ![](.resources/screenshot-tabs.png)
16
+
17
+ Right-click any tab to select a colour from the "Tab Colour" submenu.
18
+
19
+ ![](.resources/screenshot-menu.png)
20
+
21
+ ## Features
22
+
23
+ Assign colours to tabs via right-click context menu for easy visual identification when working with multiple notebooks and files.
24
+
25
+ **Colour options**:
26
+
27
+ - Red, Orange, Yellow, Green, Blue, Purple
28
+ - Pastel shades optimised for both light and dark themes
29
+
30
+ **Key features**:
31
+
32
+ - Right-click any tab to assign a colour from the "Set Tab Colour" submenu
33
+ - Colours persist across browser refreshes via localStorage
34
+ - Notebook toolbar matches the active tab colour
35
+ - Automatic cleanup when tabs are closed
36
+
37
+ Yes, this is yet another mass-produced JupyterLab extension that does one trivially simple thing. We are almost embarrassed by how utterly unremarkable it is - just some CSS classes and a context menu. But someone had to do it, and here we are. You're welcome.
38
+
39
+ ## Requirements
40
+
41
+ - JupyterLab >= 4.0.0
42
+
43
+ ## Install
44
+
45
+ To install the extension, execute:
46
+
47
+ ```bash
48
+ pip install jupyterlab_colourful_tab_extension
49
+ ```
50
+
51
+ ## Uninstall
52
+
53
+ To remove the extension, execute:
54
+
55
+ ```bash
56
+ pip uninstall jupyterlab_colourful_tab_extension
57
+ ```
package/lib/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
+ /**
3
+ * Initialization data for the jupyterlab_colourful_tab_extension extension.
4
+ */
5
+ declare const plugin: JupyterFrontEndPlugin<void>;
6
+ export default plugin;
package/lib/index.js ADDED
@@ -0,0 +1,319 @@
1
+ import { ITerminalTracker } from '@jupyterlab/terminal';
2
+ /**
3
+ * Colour definitions - CSS classes are defined in style/base.css
4
+ * CSS variables handle light/dark theme switching automatically
5
+ */
6
+ const COLOURS = [
7
+ { name: 'Red', id: 'rose', cssClass: 'jp-colourful-tab-rose' },
8
+ { name: 'Orange', id: 'peach', cssClass: 'jp-colourful-tab-peach' },
9
+ { name: 'Yellow', id: 'lemon', cssClass: 'jp-colourful-tab-lemon' },
10
+ { name: 'Green', id: 'mint', cssClass: 'jp-colourful-tab-mint' },
11
+ { name: 'Blue', id: 'sky', cssClass: 'jp-colourful-tab-sky' },
12
+ { name: 'Purple', id: 'lavender', cssClass: 'jp-colourful-tab-lavender' }
13
+ ];
14
+ /**
15
+ * LocalStorage key for persisting tab colours
16
+ */
17
+ const STORAGE_KEY = 'jupyterlab-colourful-tab-colours';
18
+ /**
19
+ * Storage for tab colours (persists across refreshes via localStorage)
20
+ * Maps widget ID to colour index
21
+ */
22
+ const tabColours = new Map();
23
+ /**
24
+ * Load tab colours from localStorage
25
+ */
26
+ function loadTabColours() {
27
+ try {
28
+ const stored = localStorage.getItem(STORAGE_KEY);
29
+ if (stored) {
30
+ const data = JSON.parse(stored);
31
+ Object.entries(data).forEach(([widgetId, colourIndex]) => {
32
+ tabColours.set(widgetId, colourIndex);
33
+ });
34
+ }
35
+ }
36
+ catch (e) {
37
+ console.warn('Colourful Tab: Failed to load colours from storage', e);
38
+ }
39
+ }
40
+ /**
41
+ * Save tab colours to localStorage
42
+ */
43
+ function saveTabColours() {
44
+ try {
45
+ const data = {};
46
+ tabColours.forEach((colourIndex, widgetId) => {
47
+ data[widgetId] = colourIndex;
48
+ });
49
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
50
+ }
51
+ catch (e) {
52
+ console.warn('Colourful Tab: Failed to save colours to storage', e);
53
+ }
54
+ }
55
+ /**
56
+ * Currently right-clicked tab element (set by contextmenu event)
57
+ */
58
+ let currentTabElement = null;
59
+ /**
60
+ * Reference to terminal tracker for getting session names
61
+ */
62
+ let terminalTracker = null;
63
+ /**
64
+ * Build a map from widget ID to terminal session name
65
+ */
66
+ function getTerminalSessionMap() {
67
+ const widgetToSession = new Map();
68
+ if (terminalTracker) {
69
+ terminalTracker.forEach(widget => {
70
+ var _a, _b;
71
+ const session = (_a = widget.content) === null || _a === void 0 ? void 0 : _a.session;
72
+ if ((_b = session === null || session === void 0 ? void 0 : session.model) === null || _b === void 0 ? void 0 : _b.name) {
73
+ widgetToSession.set(widget.id, `terminal:${session.model.name}`);
74
+ }
75
+ });
76
+ }
77
+ return widgetToSession;
78
+ }
79
+ /**
80
+ * Apply colour class to a tab element
81
+ */
82
+ function applyTabColour(tabElement, colourIndex) {
83
+ // Remove all existing colour classes
84
+ COLOURS.forEach(c => tabElement.classList.remove(c.cssClass));
85
+ // Apply new colour class
86
+ if (colourIndex >= 0 && colourIndex < COLOURS.length) {
87
+ tabElement.classList.add(COLOURS[colourIndex].cssClass);
88
+ }
89
+ }
90
+ /**
91
+ * Clear colour from a tab element
92
+ */
93
+ function clearTabColour(tabElement) {
94
+ COLOURS.forEach(c => tabElement.classList.remove(c.cssClass));
95
+ }
96
+ /**
97
+ * Get stable identifier for a tab.
98
+ * - For files: extract Path from title attribute
99
+ * - For terminals: use terminal session name (e.g., "terminal:1")
100
+ */
101
+ function getStableTabId(tabElement) {
102
+ const title = tabElement.getAttribute('title');
103
+ const widgetId = tabElement.dataset.id;
104
+ // For files: title contains "Path: /path/to/file.ipynb"
105
+ if (title && title.includes('Path:')) {
106
+ const pathMatch = title.match(/Path:\s*(.+?)(?:\n|$)/);
107
+ if (pathMatch && pathMatch[1]) {
108
+ return pathMatch[1].trim();
109
+ }
110
+ }
111
+ // For terminals: use session name which persists across browser refresh
112
+ if (widgetId) {
113
+ const terminalMap = getTerminalSessionMap();
114
+ const sessionId = terminalMap.get(widgetId);
115
+ if (sessionId) {
116
+ return sessionId;
117
+ }
118
+ // Fallback to widget ID for non-terminal widgets
119
+ return widgetId;
120
+ }
121
+ return null;
122
+ }
123
+ /**
124
+ * Find all current tabs with their stable identifiers
125
+ */
126
+ function getAllTabsByStableId() {
127
+ const idToTab = new Map();
128
+ const tabs = document.querySelectorAll('#jp-main-dock-panel .lm-TabBar-tab');
129
+ tabs.forEach(tab => {
130
+ const tabElement = tab;
131
+ const stableId = getStableTabId(tabElement);
132
+ if (stableId) {
133
+ idToTab.set(stableId, tabElement);
134
+ }
135
+ });
136
+ return idToTab;
137
+ }
138
+ /**
139
+ * Clean up colours for tabs that no longer exist
140
+ */
141
+ function cleanupStaleColours() {
142
+ const currentTabs = getAllTabsByStableId();
143
+ const staleIds = [];
144
+ tabColours.forEach((_, storedId) => {
145
+ if (!currentTabs.has(storedId)) {
146
+ staleIds.push(storedId);
147
+ }
148
+ });
149
+ if (staleIds.length > 0) {
150
+ staleIds.forEach(id => tabColours.delete(id));
151
+ saveTabColours();
152
+ }
153
+ }
154
+ /**
155
+ * Refresh all tab colours (useful after DOM changes)
156
+ */
157
+ function refreshAllTabColours() {
158
+ const currentTabs = getAllTabsByStableId();
159
+ tabColours.forEach((colourIndex, storedId) => {
160
+ const tabElement = currentTabs.get(storedId);
161
+ if (tabElement &&
162
+ !tabElement.classList.contains(COLOURS[colourIndex].cssClass)) {
163
+ applyTabColour(tabElement, colourIndex);
164
+ }
165
+ });
166
+ // Clean up colours for closed tabs
167
+ cleanupStaleColours();
168
+ }
169
+ /**
170
+ * Debounce timer for refresh
171
+ */
172
+ let refreshTimer = null;
173
+ /**
174
+ * Debounced refresh to avoid excessive DOM operations
175
+ */
176
+ function debouncedRefresh() {
177
+ if (refreshTimer) {
178
+ clearTimeout(refreshTimer);
179
+ }
180
+ refreshTimer = setTimeout(() => {
181
+ refreshAllTabColours();
182
+ applyToolbarColour();
183
+ refreshTimer = null;
184
+ }, 50);
185
+ }
186
+ /**
187
+ * Apply colour to the active notebook's toolbar based on its tab colour
188
+ */
189
+ function applyToolbarColour() {
190
+ // Find the currently active tab
191
+ const activeTab = document.querySelector('#jp-main-dock-panel .lm-TabBar-tab.lm-mod-current');
192
+ if (!activeTab) {
193
+ return;
194
+ }
195
+ // Get the stable ID for this tab
196
+ const stableId = getStableTabId(activeTab);
197
+ if (!stableId) {
198
+ return;
199
+ }
200
+ // Get the colour index for this tab
201
+ const colourIndex = tabColours.get(stableId);
202
+ // Find all toolbars and clear their colours first
203
+ const toolbars = document.querySelectorAll('jp-toolbar');
204
+ toolbars.forEach(toolbar => {
205
+ COLOURS.forEach(c => toolbar.classList.remove(c.cssClass));
206
+ });
207
+ // If the active tab has a colour, apply it to the active panel's toolbar
208
+ if (colourIndex !== undefined &&
209
+ colourIndex >= 0 &&
210
+ colourIndex < COLOURS.length) {
211
+ // Find the active panel's toolbar - it's in the currently visible notebook panel
212
+ const activePanel = document.querySelector('#jp-main-dock-panel .lm-DockPanel-widget:not(.lm-mod-hidden) jp-toolbar');
213
+ if (activePanel) {
214
+ activePanel.classList.add(COLOURS[colourIndex].cssClass);
215
+ }
216
+ }
217
+ }
218
+ /**
219
+ * Initialization data for the jupyterlab_colourful_tab_extension extension.
220
+ */
221
+ const plugin = {
222
+ id: 'jupyterlab_colourful_tab_extension:plugin',
223
+ description: 'JupyterLab extension that makes tabs coloured using pastel colours to help identify them when many are open',
224
+ autoStart: true,
225
+ optional: [ITerminalTracker],
226
+ activate: (app, tracker) => {
227
+ console.log('JupyterLab extension jupyterlab_colourful_tab_extension is activated!');
228
+ // Store terminal tracker reference for session name lookups
229
+ terminalTracker = tracker;
230
+ // Load persisted colours from localStorage
231
+ loadTabColours();
232
+ const { commands } = app;
233
+ // Track right-clicked tab using capture phase to get it before menu opens
234
+ document.addEventListener('contextmenu', (event) => {
235
+ const target = event.target;
236
+ // Only capture tabs within the main dock panel tab bar
237
+ const tabElement = target.closest('#jp-main-dock-panel .lm-DockPanel-tabBar .lm-TabBar-tab');
238
+ if (tabElement && tabElement.classList.contains('lm-TabBar-tab')) {
239
+ currentTabElement = tabElement;
240
+ }
241
+ }, true // Use capture phase
242
+ );
243
+ // Register colour commands
244
+ COLOURS.forEach((colour, index) => {
245
+ commands.addCommand(`colourful-tab:set-${colour.id}`, {
246
+ label: colour.name,
247
+ caption: `Set tab colour to ${colour.name}`,
248
+ execute: () => {
249
+ // Verify we have a valid tab element
250
+ if (currentTabElement &&
251
+ currentTabElement.classList.contains('lm-TabBar-tab')) {
252
+ const stableId = getStableTabId(currentTabElement);
253
+ if (stableId) {
254
+ tabColours.set(stableId, index);
255
+ saveTabColours();
256
+ applyTabColour(currentTabElement, index);
257
+ applyToolbarColour();
258
+ }
259
+ }
260
+ }
261
+ });
262
+ });
263
+ // Register clear command
264
+ commands.addCommand('colourful-tab:clear', {
265
+ label: 'Clear',
266
+ caption: 'Remove tab colour',
267
+ execute: () => {
268
+ // Verify we have a valid tab element
269
+ if (currentTabElement &&
270
+ currentTabElement.classList.contains('lm-TabBar-tab')) {
271
+ const stableId = getStableTabId(currentTabElement);
272
+ if (stableId) {
273
+ clearTabColour(currentTabElement);
274
+ tabColours.delete(stableId);
275
+ saveTabColours();
276
+ applyToolbarColour();
277
+ }
278
+ }
279
+ }
280
+ });
281
+ // Watch for DOM changes to reapply colours (e.g., when tabs are reordered or classes reset)
282
+ app.restored.then(() => {
283
+ const observer = new MutationObserver(mutations => {
284
+ // Check if any mutation affects tab elements
285
+ const affectsTabs = mutations.some(mutation => {
286
+ // Check for class attribute changes on tabs
287
+ if (mutation.type === 'attributes' &&
288
+ mutation.attributeName === 'class') {
289
+ const target = mutation.target;
290
+ return target.classList.contains('lm-TabBar-tab');
291
+ }
292
+ // Check for child list changes in tab bars
293
+ if (mutation.type === 'childList') {
294
+ const target = mutation.target;
295
+ return (target.classList.contains('lm-TabBar-content') ||
296
+ target.closest('.lm-TabBar') !== null);
297
+ }
298
+ return false;
299
+ });
300
+ if (affectsTabs) {
301
+ debouncedRefresh();
302
+ }
303
+ });
304
+ const dockPanel = document.getElementById('jp-main-dock-panel');
305
+ if (dockPanel) {
306
+ observer.observe(dockPanel, {
307
+ childList: true,
308
+ subtree: true,
309
+ attributes: true,
310
+ attributeFilter: ['class']
311
+ });
312
+ // Initial application of colours
313
+ refreshAllTabColours();
314
+ applyToolbarColour();
315
+ }
316
+ });
317
+ }
318
+ };
319
+ export default plugin;
package/package.json ADDED
@@ -0,0 +1,197 @@
1
+ {
2
+ "name": "jupyterlab_colourful_tab_extension",
3
+ "version": "1.0.10",
4
+ "description": "Jupyterlab extension that makes the tabs coloured (using pastel colours) to help you find tab when many of them are opened. Colour persists between refreshes",
5
+ "keywords": [
6
+ "jupyter",
7
+ "jupyterlab",
8
+ "jupyterlab-extension"
9
+ ],
10
+ "homepage": "https://github.com/stellarshenson/jupyterlab_colourful_tab_extension",
11
+ "bugs": {
12
+ "url": "https://github.com/stellarshenson/jupyterlab_colourful_tab_extension/issues"
13
+ },
14
+ "license": "BSD-3-Clause",
15
+ "author": {
16
+ "name": "Stellars Henson",
17
+ "email": "konrad.jelen@gmail.com"
18
+ },
19
+ "files": [
20
+ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
21
+ "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
22
+ "src/**/*.{ts,tsx}"
23
+ ],
24
+ "main": "lib/index.js",
25
+ "types": "lib/index.d.ts",
26
+ "style": "style/index.css",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/stellarshenson/jupyterlab_colourful_tab_extension.git"
30
+ },
31
+ "scripts": {
32
+ "build": "jlpm build:lib && jlpm build:labextension:dev",
33
+ "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
34
+ "build:labextension": "jupyter labextension build .",
35
+ "build:labextension:dev": "jupyter labextension build --development True .",
36
+ "build:lib": "tsc --sourceMap",
37
+ "build:lib:prod": "tsc",
38
+ "clean": "jlpm clean:lib",
39
+ "clean:lib": "rimraf lib tsconfig.tsbuildinfo",
40
+ "clean:lintcache": "rimraf .eslintcache .stylelintcache",
41
+ "clean:labextension": "rimraf jupyterlab_colourful_tab_extension/labextension jupyterlab_colourful_tab_extension/_version.py",
42
+ "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache",
43
+ "eslint": "jlpm eslint:check --fix",
44
+ "eslint:check": "eslint . --cache --ext .ts,.tsx",
45
+ "install:extension": "jlpm build",
46
+ "lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
47
+ "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
48
+ "prettier": "jlpm prettier:base --write --list-different",
49
+ "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
50
+ "prettier:check": "jlpm prettier:base --check",
51
+ "stylelint": "jlpm stylelint:check --fix",
52
+ "stylelint:check": "stylelint --cache \"style/**/*.css\"",
53
+ "test": "jest --coverage",
54
+ "watch": "run-p watch:src watch:labextension",
55
+ "watch:src": "tsc -w --sourceMap",
56
+ "watch:labextension": "jupyter labextension watch ."
57
+ },
58
+ "dependencies": {
59
+ "@jupyterlab/application": "^4.0.0",
60
+ "@jupyterlab/terminal": "^4.0.0",
61
+ "@lumino/widgets": "^2.0.0"
62
+ },
63
+ "devDependencies": {
64
+ "@jupyterlab/builder": "^4.0.0",
65
+ "@jupyterlab/testutils": "^4.0.0",
66
+ "@types/jest": "^29.2.0",
67
+ "@types/json-schema": "^7.0.11",
68
+ "@types/react": "^18.0.26",
69
+ "@types/react-addons-linked-state-mixin": "^0.14.22",
70
+ "@typescript-eslint/eslint-plugin": "^6.1.0",
71
+ "@typescript-eslint/parser": "^6.1.0",
72
+ "css-loader": "^6.7.1",
73
+ "eslint": "^8.36.0",
74
+ "eslint-config-prettier": "^8.8.0",
75
+ "eslint-plugin-prettier": "^5.0.0",
76
+ "jest": "^29.2.0",
77
+ "mkdirp": "^1.0.3",
78
+ "npm-run-all2": "^7.0.1",
79
+ "prettier": "^3.0.0",
80
+ "rimraf": "^5.0.1",
81
+ "source-map-loader": "^1.0.2",
82
+ "style-loader": "^3.3.1",
83
+ "stylelint": "^15.10.1",
84
+ "stylelint-config-recommended": "^13.0.0",
85
+ "stylelint-config-standard": "^34.0.0",
86
+ "stylelint-csstree-validator": "^3.0.0",
87
+ "stylelint-prettier": "^4.0.0",
88
+ "typescript": "~5.5.4"
89
+ },
90
+ "sideEffects": [
91
+ "style/*.css",
92
+ "style/index.js"
93
+ ],
94
+ "styleModule": "style/index.js",
95
+ "publishConfig": {
96
+ "access": "public"
97
+ },
98
+ "jupyterlab": {
99
+ "extension": true,
100
+ "outputDir": "jupyterlab_colourful_tab_extension/labextension",
101
+ "schemaDir": "schema"
102
+ },
103
+ "eslintIgnore": [
104
+ "node_modules",
105
+ "dist",
106
+ "coverage",
107
+ "**/*.d.ts",
108
+ "tests",
109
+ "**/__tests__",
110
+ "ui-tests"
111
+ ],
112
+ "eslintConfig": {
113
+ "extends": [
114
+ "eslint:recommended",
115
+ "plugin:@typescript-eslint/eslint-recommended",
116
+ "plugin:@typescript-eslint/recommended",
117
+ "plugin:prettier/recommended"
118
+ ],
119
+ "parser": "@typescript-eslint/parser",
120
+ "parserOptions": {
121
+ "project": "tsconfig.json",
122
+ "sourceType": "module"
123
+ },
124
+ "plugins": [
125
+ "@typescript-eslint"
126
+ ],
127
+ "rules": {
128
+ "@typescript-eslint/naming-convention": [
129
+ "error",
130
+ {
131
+ "selector": "interface",
132
+ "format": [
133
+ "PascalCase"
134
+ ],
135
+ "custom": {
136
+ "regex": "^I[A-Z]",
137
+ "match": true
138
+ }
139
+ }
140
+ ],
141
+ "@typescript-eslint/no-unused-vars": [
142
+ "warn",
143
+ {
144
+ "args": "none"
145
+ }
146
+ ],
147
+ "@typescript-eslint/no-explicit-any": "off",
148
+ "@typescript-eslint/no-namespace": "off",
149
+ "@typescript-eslint/no-use-before-define": "off",
150
+ "@typescript-eslint/quotes": [
151
+ "error",
152
+ "single",
153
+ {
154
+ "avoidEscape": true,
155
+ "allowTemplateLiterals": false
156
+ }
157
+ ],
158
+ "curly": [
159
+ "error",
160
+ "all"
161
+ ],
162
+ "eqeqeq": "error",
163
+ "prefer-arrow-callback": "error"
164
+ }
165
+ },
166
+ "prettier": {
167
+ "singleQuote": true,
168
+ "trailingComma": "none",
169
+ "arrowParens": "avoid",
170
+ "endOfLine": "auto",
171
+ "overrides": [
172
+ {
173
+ "files": "package.json",
174
+ "options": {
175
+ "tabWidth": 4
176
+ }
177
+ }
178
+ ]
179
+ },
180
+ "stylelint": {
181
+ "extends": [
182
+ "stylelint-config-recommended",
183
+ "stylelint-config-standard",
184
+ "stylelint-prettier/recommended"
185
+ ],
186
+ "plugins": [
187
+ "stylelint-csstree-validator"
188
+ ],
189
+ "rules": {
190
+ "csstree/validator": true,
191
+ "property-no-vendor-prefix": null,
192
+ "selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$",
193
+ "selector-no-vendor-prefix": null,
194
+ "value-no-vendor-prefix": null
195
+ }
196
+ }
197
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Unit tests for jupyterlab_colourful_tab_extension
3
+ */
4
+
5
+ describe('jupyterlab_colourful_tab_extension', () => {
6
+ it('should define light theme colours', () => {
7
+ const lightColours = [
8
+ { name: 'Rose', colour: '#ffd6e0' },
9
+ { name: 'Peach', colour: '#ffe5cc' },
10
+ { name: 'Lemon', colour: '#fff9c4' },
11
+ { name: 'Mint', colour: '#c8f7c5' },
12
+ { name: 'Sky', colour: '#c5e8f7' },
13
+ { name: 'Lavender', colour: '#e5d6f7' }
14
+ ];
15
+ expect(lightColours.length).toEqual(6);
16
+ });
17
+
18
+ it('should define dark theme colours', () => {
19
+ const darkColours = [
20
+ { name: 'Rose', colour: '#5c3a42' },
21
+ { name: 'Peach', colour: '#5c4a3a' },
22
+ { name: 'Lemon', colour: '#5c5a3a' },
23
+ { name: 'Mint', colour: '#3a5c3f' },
24
+ { name: 'Sky', colour: '#3a4a5c' },
25
+ { name: 'Lavender', colour: '#4a3a5c' }
26
+ ];
27
+ expect(darkColours.length).toEqual(6);
28
+ });
29
+ });
package/src/index.ts ADDED
@@ -0,0 +1,385 @@
1
+ import {
2
+ JupyterFrontEnd,
3
+ JupyterFrontEndPlugin
4
+ } from '@jupyterlab/application';
5
+ import { ITerminalTracker } from '@jupyterlab/terminal';
6
+
7
+ /**
8
+ * Colour definitions - CSS classes are defined in style/base.css
9
+ * CSS variables handle light/dark theme switching automatically
10
+ */
11
+ const COLOURS = [
12
+ { name: 'Red', id: 'rose', cssClass: 'jp-colourful-tab-rose' },
13
+ { name: 'Orange', id: 'peach', cssClass: 'jp-colourful-tab-peach' },
14
+ { name: 'Yellow', id: 'lemon', cssClass: 'jp-colourful-tab-lemon' },
15
+ { name: 'Green', id: 'mint', cssClass: 'jp-colourful-tab-mint' },
16
+ { name: 'Blue', id: 'sky', cssClass: 'jp-colourful-tab-sky' },
17
+ { name: 'Purple', id: 'lavender', cssClass: 'jp-colourful-tab-lavender' }
18
+ ];
19
+
20
+ /**
21
+ * LocalStorage key for persisting tab colours
22
+ */
23
+ const STORAGE_KEY = 'jupyterlab-colourful-tab-colours';
24
+
25
+ /**
26
+ * Storage for tab colours (persists across refreshes via localStorage)
27
+ * Maps widget ID to colour index
28
+ */
29
+ const tabColours: Map<string, number> = new Map();
30
+
31
+ /**
32
+ * Load tab colours from localStorage
33
+ */
34
+ function loadTabColours(): void {
35
+ try {
36
+ const stored = localStorage.getItem(STORAGE_KEY);
37
+ if (stored) {
38
+ const data = JSON.parse(stored) as Record<string, number>;
39
+ Object.entries(data).forEach(([widgetId, colourIndex]) => {
40
+ tabColours.set(widgetId, colourIndex);
41
+ });
42
+ }
43
+ } catch (e) {
44
+ console.warn('Colourful Tab: Failed to load colours from storage', e);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Save tab colours to localStorage
50
+ */
51
+ function saveTabColours(): void {
52
+ try {
53
+ const data: Record<string, number> = {};
54
+ tabColours.forEach((colourIndex, widgetId) => {
55
+ data[widgetId] = colourIndex;
56
+ });
57
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
58
+ } catch (e) {
59
+ console.warn('Colourful Tab: Failed to save colours to storage', e);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Currently right-clicked tab element (set by contextmenu event)
65
+ */
66
+ let currentTabElement: HTMLElement | null = null;
67
+
68
+ /**
69
+ * Reference to terminal tracker for getting session names
70
+ */
71
+ let terminalTracker: ITerminalTracker | null = null;
72
+
73
+ /**
74
+ * Build a map from widget ID to terminal session name
75
+ */
76
+ function getTerminalSessionMap(): Map<string, string> {
77
+ const widgetToSession = new Map<string, string>();
78
+ if (terminalTracker) {
79
+ terminalTracker.forEach(widget => {
80
+ const session = widget.content?.session;
81
+ if (session?.model?.name) {
82
+ widgetToSession.set(widget.id, `terminal:${session.model.name}`);
83
+ }
84
+ });
85
+ }
86
+ return widgetToSession;
87
+ }
88
+
89
+ /**
90
+ * Apply colour class to a tab element
91
+ */
92
+ function applyTabColour(tabElement: HTMLElement, colourIndex: number): void {
93
+ // Remove all existing colour classes
94
+ COLOURS.forEach(c => tabElement.classList.remove(c.cssClass));
95
+
96
+ // Apply new colour class
97
+ if (colourIndex >= 0 && colourIndex < COLOURS.length) {
98
+ tabElement.classList.add(COLOURS[colourIndex].cssClass);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Clear colour from a tab element
104
+ */
105
+ function clearTabColour(tabElement: HTMLElement): void {
106
+ COLOURS.forEach(c => tabElement.classList.remove(c.cssClass));
107
+ }
108
+
109
+ /**
110
+ * Get stable identifier for a tab.
111
+ * - For files: extract Path from title attribute
112
+ * - For terminals: use terminal session name (e.g., "terminal:1")
113
+ */
114
+ function getStableTabId(tabElement: HTMLElement): string | null {
115
+ const title = tabElement.getAttribute('title');
116
+ const widgetId = tabElement.dataset.id;
117
+
118
+ // For files: title contains "Path: /path/to/file.ipynb"
119
+ if (title && title.includes('Path:')) {
120
+ const pathMatch = title.match(/Path:\s*(.+?)(?:\n|$)/);
121
+ if (pathMatch && pathMatch[1]) {
122
+ return pathMatch[1].trim();
123
+ }
124
+ }
125
+
126
+ // For terminals: use session name which persists across browser refresh
127
+ if (widgetId) {
128
+ const terminalMap = getTerminalSessionMap();
129
+ const sessionId = terminalMap.get(widgetId);
130
+ if (sessionId) {
131
+ return sessionId;
132
+ }
133
+ // Fallback to widget ID for non-terminal widgets
134
+ return widgetId;
135
+ }
136
+
137
+ return null;
138
+ }
139
+
140
+ /**
141
+ * Find all current tabs with their stable identifiers
142
+ */
143
+ function getAllTabsByStableId(): Map<string, HTMLElement> {
144
+ const idToTab = new Map<string, HTMLElement>();
145
+ const tabs = document.querySelectorAll('#jp-main-dock-panel .lm-TabBar-tab');
146
+ tabs.forEach(tab => {
147
+ const tabElement = tab as HTMLElement;
148
+ const stableId = getStableTabId(tabElement);
149
+ if (stableId) {
150
+ idToTab.set(stableId, tabElement);
151
+ }
152
+ });
153
+ return idToTab;
154
+ }
155
+
156
+ /**
157
+ * Clean up colours for tabs that no longer exist
158
+ */
159
+ function cleanupStaleColours(): void {
160
+ const currentTabs = getAllTabsByStableId();
161
+ const staleIds: string[] = [];
162
+
163
+ tabColours.forEach((_, storedId) => {
164
+ if (!currentTabs.has(storedId)) {
165
+ staleIds.push(storedId);
166
+ }
167
+ });
168
+
169
+ if (staleIds.length > 0) {
170
+ staleIds.forEach(id => tabColours.delete(id));
171
+ saveTabColours();
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Refresh all tab colours (useful after DOM changes)
177
+ */
178
+ function refreshAllTabColours(): void {
179
+ const currentTabs = getAllTabsByStableId();
180
+
181
+ tabColours.forEach((colourIndex, storedId) => {
182
+ const tabElement = currentTabs.get(storedId);
183
+ if (
184
+ tabElement &&
185
+ !tabElement.classList.contains(COLOURS[colourIndex].cssClass)
186
+ ) {
187
+ applyTabColour(tabElement, colourIndex);
188
+ }
189
+ });
190
+
191
+ // Clean up colours for closed tabs
192
+ cleanupStaleColours();
193
+ }
194
+
195
+ /**
196
+ * Debounce timer for refresh
197
+ */
198
+ let refreshTimer: ReturnType<typeof setTimeout> | null = null;
199
+
200
+ /**
201
+ * Debounced refresh to avoid excessive DOM operations
202
+ */
203
+ function debouncedRefresh(): void {
204
+ if (refreshTimer) {
205
+ clearTimeout(refreshTimer);
206
+ }
207
+ refreshTimer = setTimeout(() => {
208
+ refreshAllTabColours();
209
+ applyToolbarColour();
210
+ refreshTimer = null;
211
+ }, 50);
212
+ }
213
+
214
+ /**
215
+ * Apply colour to the active notebook's toolbar based on its tab colour
216
+ */
217
+ function applyToolbarColour(): void {
218
+ // Find the currently active tab
219
+ const activeTab = document.querySelector(
220
+ '#jp-main-dock-panel .lm-TabBar-tab.lm-mod-current'
221
+ ) as HTMLElement;
222
+ if (!activeTab) {
223
+ return;
224
+ }
225
+
226
+ // Get the stable ID for this tab
227
+ const stableId = getStableTabId(activeTab);
228
+ if (!stableId) {
229
+ return;
230
+ }
231
+
232
+ // Get the colour index for this tab
233
+ const colourIndex = tabColours.get(stableId);
234
+
235
+ // Find all toolbars and clear their colours first
236
+ const toolbars = document.querySelectorAll('jp-toolbar');
237
+ toolbars.forEach(toolbar => {
238
+ COLOURS.forEach(c => toolbar.classList.remove(c.cssClass));
239
+ });
240
+
241
+ // If the active tab has a colour, apply it to the active panel's toolbar
242
+ if (
243
+ colourIndex !== undefined &&
244
+ colourIndex >= 0 &&
245
+ colourIndex < COLOURS.length
246
+ ) {
247
+ // Find the active panel's toolbar - it's in the currently visible notebook panel
248
+ const activePanel = document.querySelector(
249
+ '#jp-main-dock-panel .lm-DockPanel-widget:not(.lm-mod-hidden) jp-toolbar'
250
+ );
251
+ if (activePanel) {
252
+ activePanel.classList.add(COLOURS[colourIndex].cssClass);
253
+ }
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Initialization data for the jupyterlab_colourful_tab_extension extension.
259
+ */
260
+ const plugin: JupyterFrontEndPlugin<void> = {
261
+ id: 'jupyterlab_colourful_tab_extension:plugin',
262
+ description:
263
+ 'JupyterLab extension that makes tabs coloured using pastel colours to help identify them when many are open',
264
+ autoStart: true,
265
+ optional: [ITerminalTracker],
266
+ activate: (app: JupyterFrontEnd, tracker: ITerminalTracker | null) => {
267
+ console.log(
268
+ 'JupyterLab extension jupyterlab_colourful_tab_extension is activated!'
269
+ );
270
+
271
+ // Store terminal tracker reference for session name lookups
272
+ terminalTracker = tracker;
273
+
274
+ // Load persisted colours from localStorage
275
+ loadTabColours();
276
+
277
+ const { commands } = app;
278
+
279
+ // Track right-clicked tab using capture phase to get it before menu opens
280
+ document.addEventListener(
281
+ 'contextmenu',
282
+ (event: MouseEvent) => {
283
+ const target = event.target as HTMLElement;
284
+ // Only capture tabs within the main dock panel tab bar
285
+ const tabElement = target.closest(
286
+ '#jp-main-dock-panel .lm-DockPanel-tabBar .lm-TabBar-tab'
287
+ ) as HTMLElement;
288
+ if (tabElement && tabElement.classList.contains('lm-TabBar-tab')) {
289
+ currentTabElement = tabElement;
290
+ }
291
+ },
292
+ true // Use capture phase
293
+ );
294
+
295
+ // Register colour commands
296
+ COLOURS.forEach((colour, index) => {
297
+ commands.addCommand(`colourful-tab:set-${colour.id}`, {
298
+ label: colour.name,
299
+ caption: `Set tab colour to ${colour.name}`,
300
+ execute: () => {
301
+ // Verify we have a valid tab element
302
+ if (
303
+ currentTabElement &&
304
+ currentTabElement.classList.contains('lm-TabBar-tab')
305
+ ) {
306
+ const stableId = getStableTabId(currentTabElement);
307
+ if (stableId) {
308
+ tabColours.set(stableId, index);
309
+ saveTabColours();
310
+ applyTabColour(currentTabElement, index);
311
+ applyToolbarColour();
312
+ }
313
+ }
314
+ }
315
+ });
316
+ });
317
+
318
+ // Register clear command
319
+ commands.addCommand('colourful-tab:clear', {
320
+ label: 'Clear',
321
+ caption: 'Remove tab colour',
322
+ execute: () => {
323
+ // Verify we have a valid tab element
324
+ if (
325
+ currentTabElement &&
326
+ currentTabElement.classList.contains('lm-TabBar-tab')
327
+ ) {
328
+ const stableId = getStableTabId(currentTabElement);
329
+ if (stableId) {
330
+ clearTabColour(currentTabElement);
331
+ tabColours.delete(stableId);
332
+ saveTabColours();
333
+ applyToolbarColour();
334
+ }
335
+ }
336
+ }
337
+ });
338
+
339
+ // Watch for DOM changes to reapply colours (e.g., when tabs are reordered or classes reset)
340
+ app.restored.then(() => {
341
+ const observer = new MutationObserver(mutations => {
342
+ // Check if any mutation affects tab elements
343
+ const affectsTabs = mutations.some(mutation => {
344
+ // Check for class attribute changes on tabs
345
+ if (
346
+ mutation.type === 'attributes' &&
347
+ mutation.attributeName === 'class'
348
+ ) {
349
+ const target = mutation.target as HTMLElement;
350
+ return target.classList.contains('lm-TabBar-tab');
351
+ }
352
+ // Check for child list changes in tab bars
353
+ if (mutation.type === 'childList') {
354
+ const target = mutation.target as HTMLElement;
355
+ return (
356
+ target.classList.contains('lm-TabBar-content') ||
357
+ target.closest('.lm-TabBar') !== null
358
+ );
359
+ }
360
+ return false;
361
+ });
362
+
363
+ if (affectsTabs) {
364
+ debouncedRefresh();
365
+ }
366
+ });
367
+
368
+ const dockPanel = document.getElementById('jp-main-dock-panel');
369
+ if (dockPanel) {
370
+ observer.observe(dockPanel, {
371
+ childList: true,
372
+ subtree: true,
373
+ attributes: true,
374
+ attributeFilter: ['class']
375
+ });
376
+
377
+ // Initial application of colours
378
+ refreshAllTabColours();
379
+ applyToolbarColour();
380
+ }
381
+ });
382
+ }
383
+ };
384
+
385
+ export default plugin;
@@ -0,0 +1,45 @@
1
+ /*
2
+ See the JupyterLab Developer Guide for useful CSS Patterns:
3
+ https://jupyterlab.readthedocs.io/en/stable/developer/css.html
4
+ */
5
+
6
+ /* Colour selection menu */
7
+ .jp-colourful-tab-menu {
8
+ position: fixed;
9
+ z-index: 10000;
10
+ background-color: var(--jp-layout-color1);
11
+ border: 1px solid var(--jp-border-color1);
12
+ border-radius: 4px;
13
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
14
+ min-width: 140px;
15
+ padding: 4px 0;
16
+ }
17
+
18
+ .jp-colourful-tab-menu-item {
19
+ display: flex;
20
+ align-items: center;
21
+ padding: 6px 12px;
22
+ cursor: pointer;
23
+ font-size: 13px;
24
+ color: var(--jp-ui-font-color1);
25
+ }
26
+
27
+ .jp-colourful-tab-menu-item:hover {
28
+ background-color: var(--jp-layout-color2);
29
+ }
30
+
31
+ .jp-colourful-tab-swatch {
32
+ display: inline-block;
33
+ width: 16px;
34
+ height: 16px;
35
+ border-radius: 3px;
36
+ margin-right: 8px;
37
+ border: 1px solid var(--jp-border-color2);
38
+ }
39
+
40
+ .jp-colourful-tab-menu-clear {
41
+ border-top: 1px solid var(--jp-border-color1);
42
+ margin-top: 4px;
43
+ padding-top: 8px;
44
+ color: var(--jp-ui-font-color2);
45
+ }
@@ -0,0 +1 @@
1
+ @import url('base.css');
@@ -0,0 +1 @@
1
+ import './base.css';
package/style/base.css ADDED
@@ -0,0 +1,145 @@
1
+ /*
2
+ See the JupyterLab Developer Guide for useful CSS Patterns:
3
+ https://jupyterlab.readthedocs.io/en/stable/developer/css.html
4
+ */
5
+
6
+ /* Light theme pastel colours */
7
+ :root {
8
+ --jp-colourful-tab-rose: #ffd6e0;
9
+ --jp-colourful-tab-peach: #ffe5cc;
10
+ --jp-colourful-tab-lemon: #fff9c4;
11
+ --jp-colourful-tab-mint: #c8f7c5;
12
+ --jp-colourful-tab-sky: #a8d4f0;
13
+ --jp-colourful-tab-lavender: #e5d6f7;
14
+ }
15
+
16
+ /* Dark theme muted colours */
17
+ [data-jp-theme-light='false'] {
18
+ --jp-colourful-tab-rose: #5a3840;
19
+ --jp-colourful-tab-peach: #5a4a3a;
20
+ --jp-colourful-tab-lemon: #5a563a;
21
+ --jp-colourful-tab-mint: #3a5a3f;
22
+ --jp-colourful-tab-sky: #2e4258;
23
+ --jp-colourful-tab-lavender: #4a3a5a;
24
+ }
25
+
26
+ /* Colour selection menu */
27
+ .jp-colourful-tab-menu {
28
+ position: fixed;
29
+ z-index: 10000;
30
+ background-color: var(--jp-layout-color1);
31
+ border: 1px solid var(--jp-border-color1);
32
+ border-radius: 4px;
33
+ box-shadow: 0 2px 8px rgb(0 0 0 / 20%);
34
+ min-width: 140px;
35
+ padding: 4px 0;
36
+ }
37
+
38
+ .jp-colourful-tab-menu-item {
39
+ display: flex;
40
+ align-items: center;
41
+ padding: 6px 12px;
42
+ cursor: pointer;
43
+ font-size: 13px;
44
+ color: var(--jp-ui-font-color1);
45
+ }
46
+
47
+ .jp-colourful-tab-menu-item:hover {
48
+ background-color: var(--jp-layout-color2);
49
+ }
50
+
51
+ .jp-colourful-tab-swatch {
52
+ display: inline-block;
53
+ width: 16px;
54
+ height: 16px;
55
+ border-radius: 3px;
56
+ margin-right: 8px;
57
+ border: 1px solid var(--jp-border-color2);
58
+ }
59
+
60
+ /* Swatch colours */
61
+ .jp-colourful-tab-swatch-rose {
62
+ background-color: var(--jp-colourful-tab-rose);
63
+ }
64
+
65
+ .jp-colourful-tab-swatch-peach {
66
+ background-color: var(--jp-colourful-tab-peach);
67
+ }
68
+
69
+ .jp-colourful-tab-swatch-lemon {
70
+ background-color: var(--jp-colourful-tab-lemon);
71
+ }
72
+
73
+ .jp-colourful-tab-swatch-mint {
74
+ background-color: var(--jp-colourful-tab-mint);
75
+ }
76
+
77
+ .jp-colourful-tab-swatch-sky {
78
+ background-color: var(--jp-colourful-tab-sky);
79
+ }
80
+
81
+ .jp-colourful-tab-swatch-lavender {
82
+ background-color: var(--jp-colourful-tab-lavender);
83
+ }
84
+
85
+ /* Tab colours - apply to tab, inner elements, and shadow DOM parts */
86
+ .jp-colourful-tab-rose,
87
+ .jp-colourful-tab-rose * {
88
+ background-color: var(--jp-colourful-tab-rose) !important;
89
+ }
90
+
91
+ .jp-colourful-tab-rose::part(positioning-region) {
92
+ background-color: var(--jp-colourful-tab-rose) !important;
93
+ }
94
+
95
+ .jp-colourful-tab-peach,
96
+ .jp-colourful-tab-peach * {
97
+ background-color: var(--jp-colourful-tab-peach) !important;
98
+ }
99
+
100
+ .jp-colourful-tab-peach::part(positioning-region) {
101
+ background-color: var(--jp-colourful-tab-peach) !important;
102
+ }
103
+
104
+ .jp-colourful-tab-lemon,
105
+ .jp-colourful-tab-lemon * {
106
+ background-color: var(--jp-colourful-tab-lemon) !important;
107
+ }
108
+
109
+ .jp-colourful-tab-lemon::part(positioning-region) {
110
+ background-color: var(--jp-colourful-tab-lemon) !important;
111
+ }
112
+
113
+ .jp-colourful-tab-mint,
114
+ .jp-colourful-tab-mint * {
115
+ background-color: var(--jp-colourful-tab-mint) !important;
116
+ }
117
+
118
+ .jp-colourful-tab-mint::part(positioning-region) {
119
+ background-color: var(--jp-colourful-tab-mint) !important;
120
+ }
121
+
122
+ .jp-colourful-tab-sky,
123
+ .jp-colourful-tab-sky * {
124
+ background-color: var(--jp-colourful-tab-sky) !important;
125
+ }
126
+
127
+ .jp-colourful-tab-sky::part(positioning-region) {
128
+ background-color: var(--jp-colourful-tab-sky) !important;
129
+ }
130
+
131
+ .jp-colourful-tab-lavender,
132
+ .jp-colourful-tab-lavender * {
133
+ background-color: var(--jp-colourful-tab-lavender) !important;
134
+ }
135
+
136
+ .jp-colourful-tab-lavender::part(positioning-region) {
137
+ background-color: var(--jp-colourful-tab-lavender) !important;
138
+ }
139
+
140
+ .jp-colourful-tab-menu-clear {
141
+ border-top: 1px solid var(--jp-border-color1);
142
+ margin-top: 4px;
143
+ padding-top: 8px;
144
+ color: var(--jp-ui-font-color2);
145
+ }
@@ -0,0 +1 @@
1
+ @import url('base.css');
package/style/index.js ADDED
@@ -0,0 +1 @@
1
+ import './base.css';