From 0cc35c95b4a4cebf8c050f3a422b622664a8078c Mon Sep 17 00:00:00 2001 From: Lucas Oskorep Date: Thu, 26 Feb 2026 02:26:00 -0500 Subject: [PATCH] feat: add tabbed container layout mode with tab bar UI Adds a third layout mode (TABBED) alongside HORIZONTAL and VERTICAL accordion modes. In tabbed mode, only the active tab window is visible and a slim tab bar at the top of the container shows all windows with click-to-switch. Keybinding: Ctrl+/ to toggle. --- extension.ts | 1 + ...ome.shell.extensions.aerospike.gschema.xml | 6 + src/__tests__/container.test.ts | 78 ++++++ src/prefs/prefs.ts | 9 + src/wm/container.ts | 255 +++++++++++++++++- src/wm/tabBar.ts | 125 +++++++++ src/wm/window.ts | 14 + src/wm/windowManager.ts | 34 ++- stylesheet.css | 58 ++-- 9 files changed, 538 insertions(+), 42 deletions(-) create mode 100644 src/wm/tabBar.ts diff --git a/extension.ts b/extension.ts index c675673..8bd0ccf 100644 --- a/extension.ts +++ b/extension.ts @@ -39,6 +39,7 @@ export default class aerospike extends Extension { 'print-tree': () => { this.windowManager.printTreeStructure(); }, 'toggle-orientation': () => { this.windowManager.toggleActiveContainerOrientation(); }, 'reset-ratios': () => { this.windowManager.resetActiveContainerRatios(); }, + 'toggle-tabbed': () => { this.windowManager.toggleActiveContainerTabbed(); }, }; } diff --git a/schemas/org.gnome.shell.extensions.aerospike.gschema.xml b/schemas/org.gnome.shell.extensions.aerospike.gschema.xml index f0f74a4..4cbda08 100644 --- a/schemas/org.gnome.shell.extensions.aerospike.gschema.xml +++ b/schemas/org.gnome.shell.extensions.aerospike.gschema.xml @@ -55,5 +55,11 @@ Resets all window size ratios in the active window's container to equal splits + + slash']]]> + Toggle tabbed container mode + Toggles the active window's container between tabbed and accordion layout modes + + \ No newline at end of file diff --git a/src/__tests__/container.test.ts b/src/__tests__/container.test.ts index aad077e..21b29e6 100644 --- a/src/__tests__/container.test.ts +++ b/src/__tests__/container.test.ts @@ -23,9 +23,11 @@ describe('Container Logic Tests', () => { enum Orientation { HORIZONTAL = 0, VERTICAL = 1, + TABBED = 2, } const toggleOrientation = (current: Orientation): Orientation => { + if (current === Orientation.TABBED) return Orientation.HORIZONTAL; return current === Orientation.HORIZONTAL ? Orientation.VERTICAL : Orientation.HORIZONTAL; @@ -40,6 +42,17 @@ describe('Container Logic Tests', () => { const result = toggleOrientation(Orientation.VERTICAL); expect(result).toBe(Orientation.HORIZONTAL); }); + + test('should toggle from TABBED to HORIZONTAL', () => { + const result = toggleOrientation(Orientation.TABBED); + expect(result).toBe(Orientation.HORIZONTAL); + }); + + test('enum reverse mapping should return string names', () => { + expect(Orientation[Orientation.HORIZONTAL]).toBe('HORIZONTAL'); + expect(Orientation[Orientation.VERTICAL]).toBe('VERTICAL'); + expect(Orientation[Orientation.TABBED]).toBe('TABBED'); + }); }); describe('Window Bounds Calculation', () => { @@ -100,6 +113,71 @@ describe('Container Logic Tests', () => { }); }); + describe('Tabbed Bounds Calculation', () => { + const TAB_BAR_HEIGHT = 24; + + test('should give all items the same content rect in tabbed mode', () => { + const workArea = { x: 100, y: 0, width: 1000, height: 500 }; + const itemCount = 3; + + const contentRect = { + x: workArea.x, + y: workArea.y + TAB_BAR_HEIGHT, + width: workArea.width, + height: workArea.height - TAB_BAR_HEIGHT, + }; + + const bounds = Array.from({ length: itemCount }, () => contentRect); + + expect(bounds.length).toBe(3); + // All bounds should be identical + bounds.forEach(b => { + expect(b.x).toBe(100); + expect(b.y).toBe(TAB_BAR_HEIGHT); + expect(b.width).toBe(1000); + expect(b.height).toBe(500 - TAB_BAR_HEIGHT); + }); + }); + + test('tab bar rect should occupy top of work area', () => { + const workArea = { x: 200, y: 50, width: 800, height: 600 }; + + const tabBarRect = { + x: workArea.x, + y: workArea.y, + width: workArea.width, + height: TAB_BAR_HEIGHT, + }; + + expect(tabBarRect.x).toBe(200); + expect(tabBarRect.y).toBe(50); + expect(tabBarRect.width).toBe(800); + expect(tabBarRect.height).toBe(TAB_BAR_HEIGHT); + }); + + test('active tab index should clamp after removal', () => { + let activeTabIndex = 2; + const itemCount = 2; // after removing one from 3 + + if (activeTabIndex >= itemCount) { + activeTabIndex = itemCount - 1; + } + + expect(activeTabIndex).toBe(1); + }); + + test('active tab index should stay at 0 when first item removed', () => { + let activeTabIndex = 0; + const itemCount = 2; // after removing one from 3 + + if (activeTabIndex >= itemCount) { + activeTabIndex = itemCount - 1; + } + + expect(activeTabIndex).toBe(0); + }); + }); + describe('Window Index Finding', () => { test('should find window index in array', () => { const windows = [ diff --git a/src/prefs/prefs.ts b/src/prefs/prefs.ts index 4daa950..6ca591d 100644 --- a/src/prefs/prefs.ts +++ b/src/prefs/prefs.ts @@ -182,6 +182,15 @@ export default class AerospikeExtensions extends ExtensionPreferences { }) ); + keybindingsGroup.add( + new EntryRow({ + title: _('Toggle Tabbed Mode'), + settings: settings, + bind: 'toggle-tabbed', + map: keybindingMap + }) + ); + } // Helper function to create a keybinding mapping object diff --git a/src/wm/container.ts b/src/wm/container.ts index 9ff86d7..584d438 100644 --- a/src/wm/container.ts +++ b/src/wm/container.ts @@ -2,10 +2,12 @@ import {WindowWrapper} from "./window.js"; import {Logger} from "../utils/logger.js"; import queueEvent from "../utils/events.js"; import {Rect} from "../utils/rect.js"; +import {TabBar, TAB_BAR_HEIGHT} from "./tabBar.js"; -enum Orientation { +export enum Orientation { HORIZONTAL = 0, VERTICAL = 1, + TABBED = 2, } // Returns equal ratios summing exactly to 1.0, with float drift absorbed by the last slot. @@ -26,6 +28,10 @@ export default class WindowContainer { _workArea: Rect; _splitRatios: number[]; + // ── Tabbed mode state ───────────────────────────────────────────────────── + _activeTabIndex: number = 0; + _tabBar: TabBar | null = null; + constructor(workspaceArea: Rect) { this._tiledItems = []; this._tiledWindowLookup = new Map(); @@ -59,6 +65,10 @@ export default class WindowContainer { : this._workArea.height; } + isTabbed(): boolean { + return this._orientation === Orientation.TABBED; + } + // ─── Public API ───────────────────────────────────────────────────────────── move(rect: Rect): void { @@ -67,17 +77,99 @@ export default class WindowContainer { } toggleOrientation(): void { - this._orientation = this._orientation === Orientation.HORIZONTAL - ? Orientation.VERTICAL - : Orientation.HORIZONTAL; - Logger.info(`Container orientation toggled to ${this._orientation === Orientation.HORIZONTAL ? 'HORIZONTAL' : 'VERTICAL'}`); + if (this._orientation === Orientation.TABBED) { + // Tabbed → Horizontal: restore accordion mode + this.setAccordion(Orientation.HORIZONTAL); + } else { + this._orientation = this._orientation === Orientation.HORIZONTAL + ? Orientation.VERTICAL + : Orientation.HORIZONTAL; + Logger.info(`Container orientation toggled to ${Orientation[this._orientation]}`); + this.tileWindows(); + } + } + + /** + * Switch this container to tabbed mode. + */ + setTabbed(): void { + if (this._orientation === Orientation.TABBED) return; + + Logger.info("Container switching to TABBED mode"); + this._orientation = Orientation.TABBED; + + // Clamp active tab index + if (this._activeTabIndex < 0 || this._activeTabIndex >= this._tiledItems.length) { + this._activeTabIndex = 0; + } + + // Create tab bar + this._tabBar = new TabBar((index) => { + this.setActiveTab(index); + }); + this.tileWindows(); } + /** + * Switch this container back to accordion (H or V) mode. + */ + setAccordion(orientation: Orientation.HORIZONTAL | Orientation.VERTICAL): void { + if (this._orientation !== Orientation.TABBED) { + // Already accordion — just set the orientation + this._orientation = orientation; + this.tileWindows(); + return; + } + + Logger.info(`Container switching from TABBED to ${Orientation[orientation]}`); + this._orientation = orientation; + + // Destroy tab bar + if (this._tabBar) { + this._tabBar.destroy(); + this._tabBar = null; + } + + // Show all windows (they may have been hidden in tabbed mode) + this._showAllWindows(); + + this.tileWindows(); + } + + /** + * Set the active tab by index. Shows that window, hides others, updates tab bar. + */ + setActiveTab(index: number): void { + if (!this.isTabbed()) return; + if (index < 0 || index >= this._tiledItems.length) return; + + this._activeTabIndex = index; + Logger.info(`Active tab set to ${index}`); + + this._applyTabVisibility(); + this._updateTabBar(); + + // Tile to resize the active window to the content area + this.tileWindows(); + } + + getActiveTabIndex(): number { + return this._activeTabIndex; + } + addWindow(winWrap: WindowWrapper): void { this._tiledItems.push(winWrap); this._tiledWindowLookup.set(winWrap.getWindowId(), winWrap); this._addRatioForNewWindow(); + + if (this.isTabbed()) { + // In tabbed mode, hide the new window (it's not the active tab) + // and update the tab bar + this._applyTabVisibility(); + this._updateTabBar(); + } + queueEvent({ name: "tiling-windows", callback: () => this.tileWindows(), @@ -111,13 +203,29 @@ export default class WindowContainer { removeWindow(win_id: number): void { if (this._tiledWindowLookup.has(win_id)) { - // Get index before deleting from lookup to avoid race condition const index = this._getIndexOfWindow(win_id); this._tiledWindowLookup.delete(win_id); if (index !== -1) { + // If removing the window that was hidden in tabbed mode, + // make sure to show it first so it doesn't stay invisible + const item = this._tiledItems[index]; + if (item instanceof WindowWrapper) { + item.showWindow(); + } this._tiledItems.splice(index, 1); } this._resetRatios(); + + if (this.isTabbed()) { + // Clamp active tab index after removal + if (this._tiledItems.length === 0) { + this._activeTabIndex = 0; + } else if (this._activeTabIndex >= this._tiledItems.length) { + this._activeTabIndex = this._tiledItems.length - 1; + } + this._applyTabVisibility(); + this._updateTabBar(); + } } else { for (const item of this._tiledItems) { if (item instanceof WindowContainer) { @@ -139,17 +247,33 @@ export default class WindowContainer { } removeAllWindows(): void { + // Show all windows before removing (in case they were hidden in tabbed mode) + this._showAllWindows(); + + if (this._tabBar) { + this._tabBar.destroy(); + this._tabBar = null; + } + this._tiledItems = []; this._tiledWindowLookup.clear(); this._splitRatios = []; + this._activeTabIndex = 0; } tileWindows(): void { Logger.log("TILING WINDOWS IN CONTAINER"); Logger.log("WorkArea", this._workArea); - this._tileItems(); + + if (this.isTabbed()) { + this._tileTabbed(); + } else { + this._tileItems(); + } } + // ─── Accordion Tiling ─────────────────────────────────────────────────────── + _tileItems() { if (this._tiledItems.length === 0) return; @@ -166,9 +290,117 @@ export default class WindowContainer { }); } + // ─── Tabbed Tiling ────────────────────────────────────────────────────────── + + private _tileTabbed(): void { + if (this._tiledItems.length === 0) return; + + // Tab bar occupies the top of the work area + const tabBarRect: Rect = { + x: this._workArea.x, + y: this._workArea.y, + width: this._workArea.width, + height: TAB_BAR_HEIGHT, + }; + + // Content area is below the tab bar + const contentRect: Rect = { + x: this._workArea.x, + y: this._workArea.y + TAB_BAR_HEIGHT, + width: this._workArea.width, + height: this._workArea.height - TAB_BAR_HEIGHT, + }; + + // Position and show the tab bar + if (this._tabBar) { + this._tabBar.setPosition(tabBarRect); + if (!this._tabBar.isVisible()) { + this._rebuildAndShowTabBar(); + } + } + + // Resize only the active tab window to fill the content area + this._applyTabVisibility(); + + const activeItem = this._tiledItems[this._activeTabIndex]; + if (activeItem) { + if (activeItem instanceof WindowContainer) { + activeItem.move(contentRect); + } else { + Logger.info(`_tileTabbed: active tab[${this._activeTabIndex}] id=${activeItem.getWindowId()} → rect=(${contentRect.x},${contentRect.y},${contentRect.width},${contentRect.height})`); + activeItem.safelyResizeWindow(contentRect); + } + } + } + + /** + * Show the active tab window, hide all others. + */ + private _applyTabVisibility(): void { + this._tiledItems.forEach((item, index) => { + if (item instanceof WindowWrapper) { + if (index === this._activeTabIndex) { + item.showWindow(); + } else { + item.hideWindow(); + } + } + }); + } + + /** + * Show all windows (used when leaving tabbed mode). + */ + private _showAllWindows(): void { + this._tiledItems.forEach((item) => { + if (item instanceof WindowWrapper) { + item.showWindow(); + } + }); + } + + /** + * Rebuild the tab bar buttons and show it. + */ + private _rebuildAndShowTabBar(): void { + if (!this._tabBar) return; + + const windowItems = this._tiledItems.filter( + (item): item is WindowWrapper => item instanceof WindowWrapper + ); + + this._tabBar.rebuild(windowItems, this._activeTabIndex); + this._tabBar.show(); + } + + /** + * Update tab bar state (active highlight, titles) without a full rebuild. + */ + private _updateTabBar(): void { + if (!this._tabBar) return; + + // Rebuild is cheap — just recreate buttons from the current items + const windowItems = this._tiledItems.filter( + (item): item is WindowWrapper => item instanceof WindowWrapper + ); + + this._tabBar.rebuild(windowItems, this._activeTabIndex); + } + // ─── Bounds Calculation ────────────────────────────────────────────────────── getBounds(): Rect[] { + if (this._orientation === Orientation.TABBED) { + // In tabbed mode, all items share the same content rect + const contentRect: Rect = { + x: this._workArea.x, + y: this._workArea.y + TAB_BAR_HEIGHT, + width: this._workArea.width, + height: this._workArea.height - TAB_BAR_HEIGHT, + }; + return this._tiledItems.map(() => contentRect); + } + return this._orientation === Orientation.HORIZONTAL ? this._computeBounds('horizontal') : this._computeBounds('vertical'); @@ -195,6 +427,9 @@ export default class WindowContainer { // ─── Boundary Adjustment ───────────────────────────────────────────────────── adjustBoundary(boundaryIndex: number, deltaPixels: number): boolean { + // No boundary adjustment in tabbed mode + if (this.isTabbed()) return false; + if (boundaryIndex < 0 || boundaryIndex >= this._tiledItems.length - 1) { Logger.warn(`adjustBoundary: invalid boundaryIndex ${boundaryIndex}`); return false; @@ -248,6 +483,12 @@ export default class WindowContainer { // TODO: update this to work with nested containers - all other logic should already be working itemDragged(item: WindowWrapper, x: number, y: number): void { + // In tabbed mode, dragging reorders tabs but doesn't change layout + if (this.isTabbed()) { + // Don't reorder during tabbed mode — tabs have a fixed visual layout + return; + } + const original_index = this.getIndexOfItemNested(item); if (original_index === -1) { diff --git a/src/wm/tabBar.ts b/src/wm/tabBar.ts new file mode 100644 index 0000000..55c0857 --- /dev/null +++ b/src/wm/tabBar.ts @@ -0,0 +1,125 @@ +import Clutter from 'gi://Clutter'; +import St from 'gi://St'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import {Logger} from "../utils/logger.js"; +import {WindowWrapper} from "./window.js"; +import {Rect} from "../utils/rect.js"; + +export const TAB_BAR_HEIGHT = 24; + +type TabClickedCallback = (index: number) => void; + +export class TabBar { + private _bar: St.BoxLayout; + private _buttons: St.Button[] = []; + private _activeIndex: number = 0; + private _onTabClicked: TabClickedCallback; + private _visible: boolean = false; + + constructor(onTabClicked: TabClickedCallback) { + this._onTabClicked = onTabClicked; + this._bar = new St.BoxLayout({ + style_class: 'aerospike-tab-bar', + vertical: false, + reactive: true, + can_focus: false, + track_hover: false, + }); + } + + /** + * Rebuild all tab buttons from the current list of window items. + */ + rebuild(items: WindowWrapper[], activeIndex: number): void { + // Remove old buttons + this._bar.destroy_all_children(); + this._buttons = []; + + items.forEach((item, index) => { + const button = new St.Button({ + style_class: 'aerospike-tab', + reactive: true, + can_focus: false, + track_hover: true, + x_expand: true, + child: new St.Label({ + text: item.getTitle(), + style_class: 'aerospike-tab-label', + y_align: Clutter.ActorAlign.CENTER, + x_align: Clutter.ActorAlign.CENTER, + x_expand: true, + }), + }); + + button.connect('clicked', () => { + this._onTabClicked(index); + }); + + this._bar.add_child(button); + this._buttons.push(button); + }); + + this.setActive(activeIndex); + } + + /** + * Update just the title text of a single tab (e.g. when a window title changes). + */ + updateTabTitle(index: number, title: string): void { + if (index < 0 || index >= this._buttons.length) return; + const label = this._buttons[index].get_child() as St.Label; + if (label) label.set_text(title); + } + + /** + * Highlight the active tab and dim the rest. + */ + setActive(index: number): void { + this._activeIndex = index; + this._buttons.forEach((btn, i) => { + if (i === index) { + btn.add_style_class_name('aerospike-tab-active'); + } else { + btn.remove_style_class_name('aerospike-tab-active'); + } + }); + } + + /** + * Position and size the tab bar at the given screen rect. + */ + setPosition(rect: Rect): void { + this._bar.set_position(rect.x, rect.y); + this._bar.set_size(rect.width, rect.height); + } + + show(): void { + if (this._visible) return; + this._visible = true; + Main.layoutManager.uiGroup.add_child(this._bar); + this._bar.show(); + Logger.log("TabBar shown"); + } + + hide(): void { + if (!this._visible) return; + this._visible = false; + this._bar.hide(); + if (this._bar.get_parent()) { + Main.layoutManager.uiGroup.remove_child(this._bar); + } + Logger.log("TabBar hidden"); + } + + destroy(): void { + this.hide(); + this._bar.destroy_all_children(); + this._buttons = []; + this._bar.destroy(); + Logger.log("TabBar destroyed"); + } + + isVisible(): boolean { + return this._visible; + } +} diff --git a/src/wm/window.ts b/src/wm/window.ts index f6fbd54..79541c5 100644 --- a/src/wm/window.ts +++ b/src/wm/window.ts @@ -47,6 +47,20 @@ export class WindowWrapper { return this._window.get_frame_rect(); } + getTitle(): string { + return this._window.get_title() ?? 'Untitled'; + } + + hideWindow(): void { + const actor = this._window.get_compositor_private() as Clutter.Actor | null; + if (actor) actor.hide(); + } + + showWindow(): void { + const actor = this._window.get_compositor_private() as Clutter.Actor | null; + if (actor) actor.show(); + } + startDragging(): void { this._dragging = true; } diff --git a/src/wm/windowManager.ts b/src/wm/windowManager.ts index fb9906b..c0e519a 100644 --- a/src/wm/windowManager.ts +++ b/src/wm/windowManager.ts @@ -5,7 +5,7 @@ import {WindowWrapper} from './window.js'; import * as Main from "resource:///org/gnome/shell/ui/main.js"; import {Logger} from "../utils/logger.js"; import Monitor from "./monitor.js"; -import WindowContainer from "./container.js"; +import WindowContainer, {Orientation} from "./container.js"; import {Rect} from "../utils/rect.js"; @@ -329,7 +329,7 @@ export default class WindowManager implements IWindowManager { const itemIndex = container._getIndexOfWindow(winId); if (itemIndex === -1) return; - const isHorizontal = container._orientation === 0; + const isHorizontal = container._orientation === Orientation.HORIZONTAL; // E/S edge → boundary after the item; W/N edge → boundary before it. let adjusted = false; @@ -512,6 +512,29 @@ export default class WindowManager implements IWindowManager { } } + public toggleActiveContainerTabbed(): void { + if (this._activeWindowId === null) { + Logger.warn("No active window, cannot toggle tabbed mode"); + return; + } + const container = this._findContainerForWindowAcrossMonitors(this._activeWindowId); + if (container) { + if (container.isTabbed()) { + container.setAccordion(Orientation.HORIZONTAL); + } else { + // Set the active tab to the focused window + const activeIndex = container._getIndexOfWindow(this._activeWindowId); + if (activeIndex !== -1) { + container._activeTabIndex = activeIndex; + } + container.setTabbed(); + } + this._tileMonitors(); + } else { + Logger.warn("Could not find container for active window"); + } + } + public printTreeStructure(): void { Logger.info("=".repeat(80)); Logger.info("WINDOW TREE STRUCTURE"); @@ -531,8 +554,11 @@ export default class WindowManager implements IWindowManager { monitor._workspaces.forEach((workspace, workspaceIndex) => { const isActiveWorkspace = workspaceIndex === activeWorkspaceIndex; Logger.info(` Workspace ${workspaceIndex}${isActiveWorkspace && isActiveMonitor ? ' *' : ''}:`); - Logger.info(` Orientation: ${workspace._orientation === 0 ? 'HORIZONTAL' : 'VERTICAL'}`); + Logger.info(` Orientation: ${Orientation[workspace._orientation]}`); Logger.info(` Items: ${workspace._tiledItems.length}`); + if (workspace.isTabbed()) { + Logger.info(` Active Tab: ${workspace._activeTabIndex}`); + } this._printContainerTree(workspace, 4); }); }); @@ -547,7 +573,7 @@ export default class WindowManager implements IWindowManager { if (item instanceof WindowContainer) { const containsActive = this._activeWindowId !== null && item.getWindow(this._activeWindowId) !== undefined; - Logger.info(`${indent}[${index}] Container (${item._orientation === 0 ? 'HORIZONTAL' : 'VERTICAL'})${containsActive ? ' *' : ''}:`); + Logger.info(`${indent}[${index}] Container (${Orientation[item._orientation]})${containsActive ? ' *' : ''}:`); Logger.info(`${indent} Items: ${item._tiledItems.length}`); Logger.info(`${indent} Work Area: x=${item._workArea.x}, y=${item._workArea.y}, w=${item._workArea.width}, h=${item._workArea.height}`); this._printContainerTree(item, indentLevel + 4); diff --git a/stylesheet.css b/stylesheet.css index a69d151..107ccc7 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -1,37 +1,33 @@ -/* Add your custom extension styling here */ -.active-window-border { - /*border: 2px solid rgba(191, 0, 255, 0.8);*/ - /*border-radius: 3px;*/ - -/* border-image-source: linear-gradient(to left, #743ad5, #d53a9d);*/ -/* !*border: 4px solid transparent;*!*/ -/* !*border-radius: 5px;*!*/ - -/* !*!* Gradient border using border-image *!*/ -/* border-image: linear-gradient(45deg, red, orange, yellow, green, blue, indigo, violet) 1;*/ +/* ─── Tab Bar ─────────────────────────────────────────────────────────────── */ +.aerospike-tab-bar { + background-color: rgba(30, 30, 30, 0.95); + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + spacing: 1px; + padding: 2px 2px 0 2px; } -/*.border-gradient-purple {*/ -/* border-image-source: linear-gradient(to left, #743ad5, #d53a9d);*/ -/*}*/ +.aerospike-tab { + background-color: rgba(50, 50, 50, 0.8); + border-radius: 6px 6px 0 0; + padding: 2px 12px; + margin: 0 1px; + color: rgba(255, 255, 255, 0.5); + font-size: 11px; + font-weight: 400; +} -/*@keyframes rainbow-border {*/ -/* 0% {*/ -/* border-image: linear-gradient(0deg, red, orange, yellow, green, blue, indigo, violet, red) 1;*/ -/* }*/ -/* 100% {*/ -/* border-image: linear-gradient(360deg, red, orange, yellow, green, blue, indigo, violet, red) 1;*/ -/* }*/ -/*}*/ +.aerospike-tab:hover { + background-color: rgba(70, 70, 70, 0.9); + color: rgba(255, 255, 255, 0.8); +} -/*.active-window-border {*/ -/* border: 4px solid transparent;*/ -/* border-radius: 5px;*/ +.aerospike-tab-active { + background-color: rgba(80, 80, 80, 1); + color: rgba(255, 255, 255, 1); + font-weight: 500; +} -/* !* Initial gradient border *!*/ -/* border-image: linear-gradient(0deg, red, orange, yellow, green, blue, indigo, violet, red) 1;*/ - -/* !* Apply animation *!*/ -/* animation: rainbow-border 5s linear infinite;*/ -/*}*/ \ No newline at end of file +.aerospike-tab-label { + font-size: 11px; +}