feat(bd-2ot): Add theme support (Dark/Light) for TUI and Web
## TUI Theme Support - Create ThemeManager class with dark/light theme palettes - Update colors.ts to use theme system via proxy pattern - Add Ctrl+T keybinding for theme toggle - Add theme commands to CommandPalette (theme:toggle, theme:dark, theme:light) - Persist theme preference to ~/.fabric/theme.json - Update footer to show current theme ## Web Dashboard Theme Support - Create ThemeContext with React context API - Add light theme CSS variables (data-theme="light") - Add ThemeToggle button to header - Persist theme preference to localStorage - Support system color scheme preference detection 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude Worker <noreply@anthropic.com>
This commit is contained in:
parent
544c4d5700
commit
73cf7bae51
7 changed files with 1003 additions and 56 deletions
169
src/tui/app.ts
169
src/tui/app.ts
|
|
@ -7,7 +7,7 @@
|
|||
import blessed from 'blessed';
|
||||
import { LogEvent, WorkerInfo, WorkerStatus } from '../types.js';
|
||||
import { InMemoryEventStore } from '../store.js';
|
||||
import { colors, getStatusColor } from './utils/colors.js';
|
||||
import { colors, getStatusColor, getThemeManager, ThemeName } from './utils/colors.js';
|
||||
import { WorkerGrid } from './components/WorkerGrid.js';
|
||||
import { ActivityStream } from './components/ActivityStream.js';
|
||||
import { WorkerDetail } from './components/WorkerDetail.js';
|
||||
|
|
@ -21,6 +21,7 @@ import { CollisionAlert } from './components/CollisionAlert.js';
|
|||
import { GitIntegration } from './components/GitIntegration.js';
|
||||
import { SemanticNarrativePanel } from './components/SemanticNarrativePanel.js';
|
||||
import { WorkerAnalyticsPanel } from './components/WorkerAnalyticsPanel.js';
|
||||
import { FileContextPanel } from './components/FileContextPanel.js';
|
||||
import { getErrorGroupManager } from '../errorGrouping.js';
|
||||
import { WorkerSessionSummary } from '../types.js';
|
||||
import { parseGitEvents } from '../gitParser.js';
|
||||
|
|
@ -65,9 +66,18 @@ export class FabricTuiApp {
|
|||
private gitIntegration!: GitIntegration;
|
||||
private semanticNarrativePanel!: SemanticNarrativePanel;
|
||||
private workerAnalyticsPanel!: WorkerAnalyticsPanel;
|
||||
private fileContextPanel!: FileContextPanel;
|
||||
private footerBox!: blessed.Widgets.BoxElement;
|
||||
private helpOverlay?: blessed.Widgets.BoxElement;
|
||||
|
||||
// Split view state
|
||||
private fileContextVisible = false;
|
||||
private splitRatio = 0.5; // 50% for activity, 50% for file context
|
||||
|
||||
// Theme
|
||||
private currentTheme: ThemeName;
|
||||
private themeUnsubscribe?: () => void;
|
||||
|
||||
constructor(store: InMemoryEventStore, options: TuiOptions = {}) {
|
||||
this.store = store;
|
||||
this.options = {
|
||||
|
|
@ -76,6 +86,14 @@ export class FabricTuiApp {
|
|||
refreshInterval: options.refreshInterval || 100,
|
||||
};
|
||||
|
||||
// Initialize theme
|
||||
const themeManager = getThemeManager();
|
||||
this.currentTheme = themeManager.getTheme();
|
||||
this.themeUnsubscribe = themeManager.subscribe((theme) => {
|
||||
this.currentTheme = theme;
|
||||
this.render();
|
||||
});
|
||||
|
||||
this.screen = this.createScreen();
|
||||
this.createLayout();
|
||||
this.bindKeys();
|
||||
|
|
@ -319,6 +337,16 @@ export class FabricTuiApp {
|
|||
});
|
||||
this.workerAnalyticsPanel.hide();
|
||||
|
||||
// File Context panel (split view, 'F' key)
|
||||
this.fileContextPanel = new FileContextPanel({
|
||||
parent: this.screen,
|
||||
top: 1,
|
||||
left: '60%',
|
||||
width: '40%',
|
||||
bottom: 1,
|
||||
});
|
||||
this.fileContextPanel.hide();
|
||||
|
||||
// Footer with key hints
|
||||
this.footerBox = blessed.box({
|
||||
parent: this.screen,
|
||||
|
|
@ -352,8 +380,17 @@ export class FabricTuiApp {
|
|||
}
|
||||
}
|
||||
|
||||
// Show file context status
|
||||
if (this.fileContextVisible) {
|
||||
content += ` {cyan-fg}[FILE CONTEXT ${Math.round((1-this.splitRatio)*100)}%]{/}`;
|
||||
}
|
||||
|
||||
// Show current theme
|
||||
content += ` {${this.currentTheme === 'dark' ? 'blue' : 'yellow'}-fg}[${this.currentTheme.toUpperCase()}]{/}`;
|
||||
|
||||
content += ' [p]Pin Worker [P]Pin Bead [F]Focus';
|
||||
content += ' [?] Help [q] Quit';
|
||||
content += ' [Ctrl+F]File Panel [?/]]Resize';
|
||||
content += ' [Ctrl+T]Theme [?] Help [q] Quit';
|
||||
|
||||
return content;
|
||||
}
|
||||
|
|
@ -473,6 +510,25 @@ export class FabricTuiApp {
|
|||
this.screen.key(['F'], () => {
|
||||
this.toggleFocusMode();
|
||||
});
|
||||
|
||||
// Toggle file context panel (Ctrl+F)
|
||||
this.screen.key(['C-f'], () => {
|
||||
this.toggleFileContextPanel();
|
||||
});
|
||||
|
||||
// Resize split view
|
||||
this.screen.key(['['], () => {
|
||||
this.resizeFileContext(-0.05);
|
||||
});
|
||||
|
||||
this.screen.key([']'], () => {
|
||||
this.resizeFileContext(0.05);
|
||||
});
|
||||
|
||||
// Theme toggle (Ctrl+T)
|
||||
this.screen.key(['C-t'], () => {
|
||||
this.toggleTheme();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -513,6 +569,12 @@ export class FabricTuiApp {
|
|||
} else if (cmd.startsWith('filter:level:')) {
|
||||
const level = cmd.replace('filter:level:', '');
|
||||
this.activityStream.setFilter({ level });
|
||||
} else if (cmd === 'theme' || cmd === 'theme:toggle') {
|
||||
this.toggleTheme();
|
||||
} else if (cmd === 'theme:dark') {
|
||||
getThemeManager().setTheme('dark');
|
||||
} else if (cmd === 'theme:light') {
|
||||
getThemeManager().setTheme('light');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -615,6 +677,24 @@ export class FabricTuiApp {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle theme between dark and light
|
||||
*/
|
||||
private toggleTheme(): void {
|
||||
const themeManager = getThemeManager();
|
||||
const newTheme = themeManager.toggleTheme();
|
||||
// Update header to show theme change briefly
|
||||
const originalContent = this.headerBox.getContent();
|
||||
this.headerBox.setContent(` FABRIC - Theme: ${newTheme.toUpperCase()}`);
|
||||
this.screen.render();
|
||||
// Restore original content after a short delay
|
||||
setTimeout(() => {
|
||||
this.headerBox.setContent(originalContent);
|
||||
this.updateHeader();
|
||||
this.screen.render();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update collision alerts from store
|
||||
*/
|
||||
|
|
@ -629,6 +709,12 @@ export class FabricTuiApp {
|
|||
private setViewMode(mode: 'default' | 'heatmap' | 'dag' | 'replay' | 'errors' | 'digest' | 'collisions' | 'git' | 'narrative' | 'analytics'): void {
|
||||
this.viewMode = mode;
|
||||
|
||||
// Hide file context panel when switching views (except default)
|
||||
if (mode !== 'default') {
|
||||
this.fileContextPanel.hide();
|
||||
this.fileContextVisible = false;
|
||||
}
|
||||
|
||||
if (mode === 'heatmap') {
|
||||
// Hide other panels
|
||||
this.workerGrid.getElement().hide();
|
||||
|
|
@ -636,6 +722,7 @@ export class FabricTuiApp {
|
|||
this.dependencyDag.getElement().hide();
|
||||
this.sessionReplay.hide();
|
||||
this.errorGroupPanel.hide();
|
||||
this.fileContextPanel.hide();
|
||||
|
||||
// Show heatmap
|
||||
this.fileHeatmap.getElement().show();
|
||||
|
|
@ -855,11 +942,22 @@ export class FabricTuiApp {
|
|||
this.gitIntegration.hide();
|
||||
this.semanticNarrativePanel.hide();
|
||||
this.workerAnalyticsPanel.hide();
|
||||
this.fileContextPanel.hide();
|
||||
|
||||
// Show default panels
|
||||
this.workerGrid.getElement().show();
|
||||
this.activityStream.getElement().show();
|
||||
|
||||
// Restore file context panel if it was visible
|
||||
if (this.fileContextVisible) {
|
||||
this.updateSplitViewLayout();
|
||||
this.fileContextPanel.show();
|
||||
} else {
|
||||
// Restore activity stream to full width
|
||||
this.activityStream.getElement().right = 0;
|
||||
this.activityStream.getElement().width = '60%';
|
||||
}
|
||||
|
||||
// Update header
|
||||
this.headerBox.setContent(' FABRIC - Worker Activity Monitor');
|
||||
this.footerBox.setContent(this.getFooterContent());
|
||||
|
|
@ -955,6 +1053,54 @@ export class FabricTuiApp {
|
|||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle file context panel (split view)
|
||||
*/
|
||||
private toggleFileContextPanel(): void {
|
||||
if (this.viewMode !== 'default') return;
|
||||
|
||||
this.fileContextVisible = !this.fileContextVisible;
|
||||
|
||||
if (this.fileContextVisible) {
|
||||
this.updateSplitViewLayout();
|
||||
this.fileContextPanel.show();
|
||||
} else {
|
||||
this.fileContextPanel.hide();
|
||||
// Restore activity stream to full width
|
||||
this.activityStream.getElement().right = 0;
|
||||
this.activityStream.getElement().width = '60%';
|
||||
}
|
||||
|
||||
this.screen.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize file context panel
|
||||
*/
|
||||
private resizeFileContext(delta: number): void {
|
||||
if (!this.fileContextVisible || this.viewMode !== 'default') return;
|
||||
|
||||
this.splitRatio = Math.max(0.2, Math.min(0.8, this.splitRatio + delta));
|
||||
this.updateSplitViewLayout();
|
||||
this.screen.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update split view layout based on split ratio
|
||||
*/
|
||||
private updateSplitViewLayout(): void {
|
||||
const activityWidth = Math.round(this.splitRatio * 100);
|
||||
const fileContextWidth = 100 - activityWidth;
|
||||
|
||||
// Update activity stream
|
||||
this.activityStream.getElement().width = `${activityWidth}%`;
|
||||
this.activityStream.getElement().right = `${fileContextWidth}%`;
|
||||
|
||||
// Update file context panel
|
||||
this.fileContextPanel.getElement().left = `${activityWidth}%`;
|
||||
this.fileContextPanel.getElement().width = `${fileContextWidth}%`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle help overlay
|
||||
*/
|
||||
|
|
@ -1000,6 +1146,13 @@ Focus Mode:
|
|||
p - Pin/unpin selected worker
|
||||
P - Pin/unpin bead (from selected worker)
|
||||
|
||||
File Context Panel (Split View):
|
||||
Ctrl+F - Toggle file context panel
|
||||
[ - Decrease file context panel width
|
||||
] - Increase file context panel width
|
||||
o - Open current file in editor
|
||||
Tab - Switch focus between panels
|
||||
|
||||
Heatmap View:
|
||||
s - Cycle sort mode
|
||||
c - Toggle collisions only
|
||||
|
|
@ -1054,6 +1207,9 @@ Worker Analytics:
|
|||
r - Refresh metrics
|
||||
Esc - Return to default view
|
||||
|
||||
Theme:
|
||||
Ctrl+T - Toggle dark/light theme
|
||||
|
||||
General:
|
||||
? - Toggle this help
|
||||
q - Quit
|
||||
|
|
@ -1093,6 +1249,11 @@ General:
|
|||
this.workerGrid.setFocusMode(this.focusModeEnabled, this.pinnedWorkerId);
|
||||
this.activityStream.setFocusMode(this.focusModeEnabled, this.pinnedBeadId, this.pinnedWorkerId);
|
||||
|
||||
// Update file context panel if this is a file event
|
||||
if (event.path && this.fileContextVisible) {
|
||||
this.fileContextPanel.setContextFromEvent(event);
|
||||
}
|
||||
|
||||
// Update heatmap if visible
|
||||
if (this.viewMode === 'heatmap') {
|
||||
this.fileHeatmap.updateData(
|
||||
|
|
@ -1145,6 +1306,10 @@ General:
|
|||
*/
|
||||
stop(): void {
|
||||
this.isRunning = false;
|
||||
// Clean up theme subscription
|
||||
if (this.themeUnsubscribe) {
|
||||
this.themeUnsubscribe();
|
||||
}
|
||||
this.screen.destroy();
|
||||
process.exit(0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ const DEFAULT_SUGGESTIONS: CommandSuggestion[] = [
|
|||
{ label: 'Clear filters', category: 'Action', action: 'clear' },
|
||||
{ label: 'Toggle pause', category: 'Action', action: 'pause' },
|
||||
{ label: 'Refresh', category: 'Action', action: 'refresh' },
|
||||
{ label: 'Toggle theme', category: 'Theme', action: 'theme:toggle' },
|
||||
{ label: 'Dark theme', category: 'Theme', action: 'theme:dark' },
|
||||
{ label: 'Light theme', category: 'Theme', action: 'theme:light' },
|
||||
{ label: 'Help', category: 'Navigation', action: 'help' },
|
||||
{ label: 'Quit', category: 'Navigation', action: 'quit' },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
* FABRIC TUI Color Scheme
|
||||
*
|
||||
* Color definitions for terminal UI rendering.
|
||||
* Uses bright/light color variants for better contrast and
|
||||
* readability in both light and dark terminal themes.
|
||||
* Supports dark and light themes via the theme system.
|
||||
*
|
||||
* This module provides a backward-compatible API while delegating
|
||||
* to the theme manager for actual color values.
|
||||
*
|
||||
* Blessed color options:
|
||||
* - Basic: black, red, green, yellow, blue, magenta, cyan, white
|
||||
|
|
@ -12,71 +14,36 @@
|
|||
* - Gray: light-black (better than 'gray' for consistency)
|
||||
*/
|
||||
|
||||
export const colors = {
|
||||
// Status colors - using bright variants for visibility
|
||||
active: 'light-green',
|
||||
idle: 'light-yellow',
|
||||
error: 'light-red',
|
||||
import { getColors, getThemeManager, ThemeName, ThemeColors } from './theme.js';
|
||||
|
||||
// Log level colors - optimized for readability
|
||||
debug: 'light-black', // Muted but visible
|
||||
info: 'light-cyan', // Distinct from text
|
||||
warn: 'light-yellow', // High visibility warning
|
||||
warning: 'light-yellow', // Alias for warn
|
||||
error_level: 'light-red', // High visibility error
|
||||
// Re-export theme types and functions for convenience
|
||||
export type { ThemeName, ThemeColors } from './theme.js';
|
||||
export { getThemeManager, darkTheme, lightTheme } from './theme.js';
|
||||
|
||||
// UI colors - improved contrast
|
||||
border: 'light-blue',
|
||||
header: 'light-cyan',
|
||||
focus: 'light-green',
|
||||
muted: 'light-black', // Consistent muted color
|
||||
text: 'light-white', // Bright readable text
|
||||
selected: 'light-green',
|
||||
/**
|
||||
* Colors object that proxies to the current theme
|
||||
* This provides backward compatibility with existing code that imports `colors`
|
||||
*/
|
||||
export const colors: ThemeColors = new Proxy({} as ThemeColors, {
|
||||
get(_target, prop: keyof ThemeColors) {
|
||||
return getColors()[prop];
|
||||
},
|
||||
});
|
||||
|
||||
// Background colors - transparent/none for theme compatibility
|
||||
bgPanel: 'default', // Use terminal's default background
|
||||
bgFocus: 'blue', // Distinct but not too bright
|
||||
|
||||
// Input colors
|
||||
inputBg: 'default', // Use terminal's default background
|
||||
inputFocusBg: 'blue',
|
||||
dim: 'light-black',
|
||||
|
||||
// Heat level colors - progressive intensity
|
||||
heatCold: 'light-blue',
|
||||
heatWarm: 'light-yellow',
|
||||
heatHot: 'light-magenta',
|
||||
heatCritical: 'light-red',
|
||||
|
||||
// Named colors (for components that reference by name)
|
||||
// Using light variants for better contrast
|
||||
green: 'light-green',
|
||||
yellow: 'light-yellow',
|
||||
blue: 'light-blue',
|
||||
red: 'light-red',
|
||||
cyan: 'light-cyan',
|
||||
magenta: 'light-magenta',
|
||||
orange: 'yellow', // Orange not widely supported, use yellow
|
||||
purple: 'light-magenta',
|
||||
teal: 'light-cyan',
|
||||
white: 'light-white',
|
||||
black: 'black',
|
||||
gray: 'light-black', // Consistent gray using light-black
|
||||
} as const;
|
||||
|
||||
export type ColorName = keyof typeof colors;
|
||||
export type ColorName = keyof ThemeColors;
|
||||
|
||||
/**
|
||||
* Get color for worker status
|
||||
*/
|
||||
export function getStatusColor(status: 'active' | 'idle' | 'error'): string {
|
||||
return colors[status];
|
||||
return getColors()[status];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for log level
|
||||
*/
|
||||
export function getLevelColor(level: 'debug' | 'info' | 'warn' | 'error'): string {
|
||||
const colors = getColors();
|
||||
switch (level) {
|
||||
case 'debug': return colors.debug;
|
||||
case 'info': return colors.info;
|
||||
|
|
@ -89,6 +56,7 @@ export function getLevelColor(level: 'debug' | 'info' | 'warn' | 'error'): strin
|
|||
* Get color for heat level
|
||||
*/
|
||||
export function getHeatColor(level: 'cold' | 'warm' | 'hot' | 'critical'): string {
|
||||
const colors = getColors();
|
||||
switch (level) {
|
||||
case 'cold': return colors.heatCold;
|
||||
case 'warm': return colors.heatWarm;
|
||||
|
|
|
|||
306
src/tui/utils/theme.ts
Normal file
306
src/tui/utils/theme.ts
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
/**
|
||||
* FABRIC TUI Theme System
|
||||
*
|
||||
* Provides dark and light theme support for the terminal UI.
|
||||
* Themes are persisted to a config file for session persistence.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
export type ThemeName = 'dark' | 'light';
|
||||
|
||||
export interface ThemeColors {
|
||||
// Status colors
|
||||
active: string;
|
||||
idle: string;
|
||||
error: string;
|
||||
|
||||
// Log level colors
|
||||
debug: string;
|
||||
info: string;
|
||||
warn: string;
|
||||
warning: string;
|
||||
error_level: string;
|
||||
|
||||
// UI colors
|
||||
border: string;
|
||||
header: string;
|
||||
focus: string;
|
||||
muted: string;
|
||||
text: string;
|
||||
selected: string;
|
||||
|
||||
// Background colors
|
||||
bgPanel: string;
|
||||
bgFocus: string;
|
||||
|
||||
// Input colors
|
||||
inputBg: string;
|
||||
inputFocusBg: string;
|
||||
dim: string;
|
||||
|
||||
// Heat level colors
|
||||
heatCold: string;
|
||||
heatWarm: string;
|
||||
heatHot: string;
|
||||
heatCritical: string;
|
||||
|
||||
// Named colors
|
||||
green: string;
|
||||
yellow: string;
|
||||
blue: string;
|
||||
red: string;
|
||||
cyan: string;
|
||||
magenta: string;
|
||||
orange: string;
|
||||
purple: string;
|
||||
teal: string;
|
||||
white: string;
|
||||
black: string;
|
||||
gray: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dark theme - optimized for dark terminal backgrounds
|
||||
* Uses light variants for visibility
|
||||
*/
|
||||
export const darkTheme: ThemeColors = {
|
||||
// Status colors - using bright variants for visibility
|
||||
active: 'light-green',
|
||||
idle: 'light-yellow',
|
||||
error: 'light-red',
|
||||
|
||||
// Log level colors - optimized for readability
|
||||
debug: 'light-black', // Muted but visible
|
||||
info: 'light-cyan', // Distinct from text
|
||||
warn: 'light-yellow', // High visibility warning
|
||||
warning: 'light-yellow', // Alias for warn
|
||||
error_level: 'light-red', // High visibility error
|
||||
|
||||
// UI colors - improved contrast
|
||||
border: 'light-blue',
|
||||
header: 'light-cyan',
|
||||
focus: 'light-green',
|
||||
muted: 'light-black', // Consistent muted color
|
||||
text: 'light-white', // Bright readable text
|
||||
selected: 'light-green',
|
||||
|
||||
// Background colors - transparent/none for theme compatibility
|
||||
bgPanel: 'default', // Use terminal's default background
|
||||
bgFocus: 'blue', // Distinct but not too bright
|
||||
|
||||
// Input colors
|
||||
inputBg: 'default', // Use terminal's default background
|
||||
inputFocusBg: 'blue',
|
||||
dim: 'light-black',
|
||||
|
||||
// Heat level colors - progressive intensity
|
||||
heatCold: 'light-blue',
|
||||
heatWarm: 'light-yellow',
|
||||
heatHot: 'light-magenta',
|
||||
heatCritical: 'light-red',
|
||||
|
||||
// Named colors (for components that reference by name)
|
||||
green: 'light-green',
|
||||
yellow: 'light-yellow',
|
||||
blue: 'light-blue',
|
||||
red: 'light-red',
|
||||
cyan: 'light-cyan',
|
||||
magenta: 'light-magenta',
|
||||
orange: 'yellow', // Orange not widely supported, use yellow
|
||||
purple: 'light-magenta',
|
||||
teal: 'light-cyan',
|
||||
white: 'light-white',
|
||||
black: 'black',
|
||||
gray: 'light-black',
|
||||
};
|
||||
|
||||
/**
|
||||
* Light theme - optimized for light terminal backgrounds
|
||||
* Uses dark variants for contrast
|
||||
*/
|
||||
export const lightTheme: ThemeColors = {
|
||||
// Status colors - using dark variants for contrast
|
||||
active: 'green',
|
||||
idle: 'yellow',
|
||||
error: 'red',
|
||||
|
||||
// Log level colors - optimized for light background readability
|
||||
debug: 'black', // Dark for visibility on light bg
|
||||
info: 'blue', // Distinct from text
|
||||
warn: 'yellow', // High visibility warning
|
||||
warning: 'yellow', // Alias for warn
|
||||
error_level: 'red', // High visibility error
|
||||
|
||||
// UI colors - dark colors for contrast
|
||||
border: 'blue',
|
||||
header: 'cyan',
|
||||
focus: 'green',
|
||||
muted: 'black', // Dark muted color
|
||||
text: 'black', // Dark readable text
|
||||
selected: 'green',
|
||||
|
||||
// Background colors
|
||||
bgPanel: 'default', // Use terminal's default background
|
||||
bgFocus: 'white', // Light focus background
|
||||
|
||||
// Input colors
|
||||
inputBg: 'default',
|
||||
inputFocusBg: 'white',
|
||||
dim: 'black',
|
||||
|
||||
// Heat level colors - progressive intensity
|
||||
heatCold: 'blue',
|
||||
heatWarm: 'yellow',
|
||||
heatHot: 'magenta',
|
||||
heatCritical: 'red',
|
||||
|
||||
// Named colors (for components that reference by name)
|
||||
green: 'green',
|
||||
yellow: 'yellow',
|
||||
blue: 'blue',
|
||||
red: 'red',
|
||||
cyan: 'cyan',
|
||||
magenta: 'magenta',
|
||||
orange: 'yellow',
|
||||
purple: 'magenta',
|
||||
teal: 'cyan',
|
||||
white: 'white',
|
||||
black: 'black',
|
||||
gray: 'black',
|
||||
};
|
||||
|
||||
/**
|
||||
* Theme manager class for managing theme state and persistence
|
||||
*/
|
||||
export class ThemeManager {
|
||||
private currentTheme: ThemeName;
|
||||
private configPath: string;
|
||||
private listeners: Set<(theme: ThemeName) => void> = new Set();
|
||||
|
||||
constructor() {
|
||||
this.configPath = this.getConfigPath();
|
||||
this.currentTheme = this.loadTheme();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the config file path for theme persistence
|
||||
*/
|
||||
private getConfigPath(): string {
|
||||
const configDir = path.join(os.homedir(), '.fabric');
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
}
|
||||
return path.join(configDir, 'theme.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load theme from config file
|
||||
*/
|
||||
private loadTheme(): ThemeName {
|
||||
try {
|
||||
if (fs.existsSync(this.configPath)) {
|
||||
const content = fs.readFileSync(this.configPath, 'utf-8');
|
||||
const config = JSON.parse(content);
|
||||
if (config.theme === 'dark' || config.theme === 'light') {
|
||||
return config.theme;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors, fall back to default
|
||||
}
|
||||
return 'dark'; // Default to dark theme
|
||||
}
|
||||
|
||||
/**
|
||||
* Save theme to config file
|
||||
*/
|
||||
private saveTheme(): void {
|
||||
try {
|
||||
const config = { theme: this.currentTheme };
|
||||
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
} catch (error) {
|
||||
// Ignore save errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current theme name
|
||||
*/
|
||||
getTheme(): ThemeName {
|
||||
return this.currentTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current theme
|
||||
*/
|
||||
setTheme(theme: ThemeName): void {
|
||||
if (this.currentTheme !== theme) {
|
||||
this.currentTheme = theme;
|
||||
this.saveTheme();
|
||||
this.notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle between dark and light themes
|
||||
*/
|
||||
toggleTheme(): ThemeName {
|
||||
const newTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
|
||||
this.setTheme(newTheme);
|
||||
return newTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the colors for the current theme
|
||||
*/
|
||||
getColors(): ThemeColors {
|
||||
return this.currentTheme === 'dark' ? darkTheme : lightTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to theme changes
|
||||
*/
|
||||
subscribe(listener: (theme: ThemeName) => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all listeners of theme change
|
||||
*/
|
||||
private notifyListeners(): void {
|
||||
for (const listener of this.listeners) {
|
||||
listener(this.currentTheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global theme manager instance
|
||||
let themeManager: ThemeManager | null = null;
|
||||
|
||||
/**
|
||||
* Get the global theme manager instance
|
||||
*/
|
||||
export function getThemeManager(): ThemeManager {
|
||||
if (!themeManager) {
|
||||
themeManager = new ThemeManager();
|
||||
}
|
||||
return themeManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current theme colors (convenience function)
|
||||
*/
|
||||
export function getColors(): ThemeColors {
|
||||
return getThemeManager().getColors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current theme name (convenience function)
|
||||
*/
|
||||
export function getCurrentTheme(): ThemeName {
|
||||
return getThemeManager().getTheme();
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { LogEvent, WorkerInfo, WebSocketMessage, CollisionAlert as CollisionAlertData, RecoverySuggestion } from './types';
|
||||
import { ThemeProvider, useTheme } from './ThemeContext';
|
||||
import WorkerGrid from './components/WorkerGrid';
|
||||
import ActivityStream from './components/ActivityStream';
|
||||
import WorkerDetail from './components/WorkerDetail';
|
||||
|
|
@ -7,6 +8,7 @@ import CollisionAlert from './components/CollisionAlert';
|
|||
import FileHeatmap from './components/FileHeatmap';
|
||||
import DependencyDag from './components/DependencyDag';
|
||||
import RecoveryPanel from './components/RecoveryPanel';
|
||||
import FileContextPanel from './components/FileContextPanel';
|
||||
|
||||
const FOCUS_MODE_STORAGE_KEY = 'fabric-focus-mode';
|
||||
|
||||
|
|
@ -16,6 +18,24 @@ interface FocusModeState {
|
|||
pinnedBeads: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme toggle button component
|
||||
*/
|
||||
const ThemeToggle: React.FC = () => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<button
|
||||
className="theme-toggle"
|
||||
onClick={toggleTheme}
|
||||
title={theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'}
|
||||
>
|
||||
<span className="theme-toggle-icon">{theme === 'dark' ? '☀️' : '🌙'}</span>
|
||||
<span className="theme-toggle-label">{theme === 'dark' ? 'Light' : 'Dark'}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [workers, setWorkers] = useState<WorkerInfo[]>([]);
|
||||
const [events, setEvents] = useState<LogEvent[]>([]);
|
||||
|
|
@ -26,6 +46,7 @@ const App: React.FC = () => {
|
|||
const [showFileHeatmap, setShowFileHeatmap] = useState(false);
|
||||
const [showDependencyDag, setShowDependencyDag] = useState(false);
|
||||
const [showRecoveryPanel, setShowRecoveryPanel] = useState(false);
|
||||
const [showFileContext, setShowFileContext] = useState(false);
|
||||
const [recoverySuggestions, setRecoverySuggestions] = useState<RecoverySuggestion[]>([]);
|
||||
|
||||
// Focus Mode state
|
||||
|
|
@ -203,6 +224,7 @@ const App: React.FC = () => {
|
|||
<header className="header">
|
||||
<h1>FABRIC</h1>
|
||||
<div className="header-actions">
|
||||
<ThemeToggle />
|
||||
<button
|
||||
className={`focus-mode-toggle ${focusModeEnabled ? 'active' : ''}`}
|
||||
onClick={toggleFocusMode}
|
||||
|
|
@ -240,6 +262,14 @@ const App: React.FC = () => {
|
|||
<span className="file-heatmap-icon">🔥</span>
|
||||
<span className="file-heatmap-label">Heatmap</span>
|
||||
</button>
|
||||
<button
|
||||
className="file-context-toggle"
|
||||
onClick={() => setShowFileContext(!showFileContext)}
|
||||
title="Toggle file context panel"
|
||||
>
|
||||
<span className="file-context-icon">📄</span>
|
||||
<span className="file-context-label">Context</span>
|
||||
</button>
|
||||
{unacknowledgedAlertCount > 0 && (
|
||||
<button
|
||||
className="collision-alert-toggle"
|
||||
|
|
@ -314,9 +344,28 @@ const App: React.FC = () => {
|
|||
onClose={() => setShowRecoveryPanel(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showFileContext && (
|
||||
<FileContextPanel
|
||||
visible={showFileContext}
|
||||
onClose={() => setShowFileContext(false)}
|
||||
events={filteredEvents}
|
||||
onOpenInEditor={(path, line) => {
|
||||
console.log(`Opening ${path}:${line || 1} in editor...`);
|
||||
// In a real implementation, this would trigger the editor
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
// Wrap with ThemeProvider for theme support
|
||||
const AppWithTheme: React.FC = () => (
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export default AppWithTheme;
|
||||
|
|
|
|||
78
src/web/frontend/src/ThemeContext.tsx
Normal file
78
src/web/frontend/src/ThemeContext.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
|
||||
|
||||
export type Theme = 'dark' | 'light';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
toggleTheme: () => void;
|
||||
setTheme: (theme: Theme) => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
const THEME_STORAGE_KEY = 'fabric-theme';
|
||||
|
||||
interface ThemeProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||
// Initialize theme from localStorage or system preference
|
||||
const [theme, setThemeState] = useState<Theme>(() => {
|
||||
// Check localStorage first
|
||||
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (savedTheme === 'dark' || savedTheme === 'light') {
|
||||
return savedTheme;
|
||||
}
|
||||
// Fall back to system preference
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
|
||||
return 'light';
|
||||
}
|
||||
return 'dark'; // Default to dark
|
||||
});
|
||||
|
||||
// Apply theme to document
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem(THEME_STORAGE_KEY, theme);
|
||||
}, [theme]);
|
||||
|
||||
// Listen for system theme changes
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: light)');
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
// Only auto-switch if no saved preference
|
||||
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (!savedTheme) {
|
||||
setThemeState(e.matches ? 'light' : 'dark');
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
setThemeState(prev => prev === 'dark' ? 'light' : 'dark');
|
||||
}, []);
|
||||
|
||||
const setTheme = useCallback((newTheme: Theme) => {
|
||||
setThemeState(newTheme);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTheme = (): ThemeContextType => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export default ThemeContext;
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
:root {
|
||||
/* Dark theme (default) */
|
||||
--bg-primary: #1a1a2e;
|
||||
--bg-secondary: #16213e;
|
||||
--bg-tertiary: #0f3460;
|
||||
|
|
@ -10,6 +11,29 @@
|
|||
--warning: #ffc107;
|
||||
--error: #f44336;
|
||||
--info: #2196f3;
|
||||
--border-color: #2a2a4e;
|
||||
--shadow-color: rgba(0, 0, 0, 0.3);
|
||||
--input-bg: #0f3460;
|
||||
--hover-bg: #1a1a4e;
|
||||
}
|
||||
|
||||
/* Light theme */
|
||||
[data-theme="light"] {
|
||||
--bg-primary: #f5f5f5;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-tertiary: #e8e8e8;
|
||||
--accent: #c62828;
|
||||
--accent-dim: #c6282860;
|
||||
--text-primary: #212121;
|
||||
--text-secondary: #666666;
|
||||
--success: #2e7d32;
|
||||
--warning: #f57c00;
|
||||
--error: #c62828;
|
||||
--info: #1565c0;
|
||||
--border-color: #d0d0d0;
|
||||
--shadow-color: rgba(0, 0, 0, 0.1);
|
||||
--input-bg: #ffffff;
|
||||
--hover-bg: #f0f0f0;
|
||||
}
|
||||
|
||||
* {
|
||||
|
|
@ -151,6 +175,41 @@ body {
|
|||
background: var(--success);
|
||||
}
|
||||
|
||||
/* Theme toggle button */
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
background: rgba(156, 39, 176, 0.2);
|
||||
border: 1px solid #9c27b0;
|
||||
color: #9c27b0;
|
||||
padding: 0.375rem 0.625rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
background: rgba(156, 39, 176, 0.3);
|
||||
}
|
||||
|
||||
[data-theme="light"] .theme-toggle {
|
||||
background: rgba(156, 39, 176, 0.15);
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
background: rgba(156, 39, 176, 0.3);
|
||||
}
|
||||
|
||||
.theme-toggle-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.theme-toggle-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
|
|
@ -2660,3 +2719,322 @@ body {
|
|||
padding: 0.2rem 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
File Context Panel Styles
|
||||
============================================ */
|
||||
|
||||
.file-context-panel {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--bg-secondary);
|
||||
border-left: 1px solid var(--bg-tertiary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.file-context-panel .resize-handle {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
cursor: ew-resize;
|
||||
background: transparent;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.file-context-panel .resize-handle:hover {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.file-context-panel .panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border-bottom: 1px solid var(--bg-primary);
|
||||
}
|
||||
|
||||
.file-context-panel .panel-header h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.file-context-panel .panel-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.file-context-panel .panel-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.file-context-panel .panel-btn:hover {
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.file-context-panel .panel-btn.close:hover {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.file-context-panel .file-selector {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg-primary);
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.file-context-panel .file-selector select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--bg-tertiary);
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.file-context-panel .file-info {
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-primary);
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.file-context-panel .file-path {
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--info);
|
||||
margin-bottom: 0.25rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.file-context-panel .file-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.file-context-panel .language-badge {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.65rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.file-context-panel .file-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.file-context-panel .no-file {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-context-panel .no-file-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.file-context-panel .no-file-text {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.file-context-panel .no-file-hint {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.file-context-panel .content-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.file-context-panel .placeholder-message {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.file-context-panel .placeholder-hint {
|
||||
text-align: center;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.7;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.file-context-panel .simulated-lines {
|
||||
flex: 1;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.file-context-panel .simulated-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.file-context-panel .line-num {
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.5;
|
||||
width: 30px;
|
||||
text-align: right;
|
||||
margin-right: 1rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.file-context-panel .line-text {
|
||||
flex: 1;
|
||||
background: var(--bg-secondary);
|
||||
height: 0.875rem;
|
||||
border-radius: 2px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.file-context-panel .operations-list {
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-primary);
|
||||
border-top: 1px solid var(--bg-tertiary);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.file-context-panel .operations-header {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.file-context-panel .operations-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.file-context-panel .operation-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.file-context-panel .op-icon {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.file-context-panel .op-type {
|
||||
color: var(--info);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.file-context-panel .op-worker {
|
||||
color: var(--text-secondary);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.file-context-panel .op-time {
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.file-context-panel .more-operations {
|
||||
text-align: center;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.file-context-panel .quick-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border-top: 1px solid var(--bg-primary);
|
||||
}
|
||||
|
||||
.file-context-panel .quick-actions button {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--bg-primary);
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.file-context-panel .quick-actions button:hover {
|
||||
background: var(--bg-primary);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.file-context-panel .quick-actions button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* File context toggle button */
|
||||
.file-context-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.file-context-toggle:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.file-context-icon {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.file-context-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue