refactor: Flatten directory structure
Move project files from uzdb/ subdirectory to root directory for cleaner project structure.
Changes:
- Move frontend/ to root
- Move internal/ to root
- Move build/ to root
- Move all config files (go.mod, wails.json, etc.) to root
- Remove redundant uzdb/ subdirectory nesting
Project structure is now:
├── frontend/ # React application
├── internal/ # Go backend
├── build/ # Wails build assets
├── doc/ # Design documentation
├── main.go # Entry point
└── ...
🤖 Generated with Qoder
This commit is contained in:
134
frontend/src/components/Layout/AppLayout.css
Normal file
134
frontend/src/components/Layout/AppLayout.css
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* AppLayout Component Styles
|
||||
*/
|
||||
|
||||
.app-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Menu Bar */
|
||||
.layout-menubar {
|
||||
flex-shrink: 0;
|
||||
height: var(--menubar-height);
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
.layout-toolbar {
|
||||
flex-shrink: 0;
|
||||
height: var(--toolbar-height);
|
||||
background-color: var(--bg-primary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Main area (sidebar + workspace) */
|
||||
.layout-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.layout-sidebar {
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
transition: width var(--transition-normal) var(--ease-in-out);
|
||||
}
|
||||
|
||||
.layout-sidebar.collapsed {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Resize handle */
|
||||
.layout-resize-handle {
|
||||
width: 4px;
|
||||
cursor: col-resize;
|
||||
background-color: transparent;
|
||||
transition: background-color var(--transition-fast) var(--ease-in-out);
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.layout-resize-handle:hover {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
.layout-resize-handle:active {
|
||||
background-color: var(--primary-active);
|
||||
}
|
||||
|
||||
/* Sidebar toggle button */
|
||||
.layout-sidebar-toggle {
|
||||
width: 48px;
|
||||
background-color: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: var(--space-2);
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Workspace */
|
||||
.layout-workspace {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
/* Status Bar */
|
||||
.layout-statusbar {
|
||||
flex-shrink: 0;
|
||||
height: var(--statusbar-height);
|
||||
background-color: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.layout-sidebar:not(.collapsed) {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.layout-resize-handle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for sidebar transitions */
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-sidebar:not(.collapsed) {
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
155
frontend/src/components/Layout/AppLayout.tsx
Normal file
155
frontend/src/components/Layout/AppLayout.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* AppLayout Component
|
||||
*
|
||||
* Main application layout with menu bar, toolbar, sidebar, main content area, and status bar.
|
||||
* Based on layout-design.md "整体布局架构"
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import './AppLayout.css';
|
||||
|
||||
export interface AppLayoutProps {
|
||||
/** Menu bar component */
|
||||
menuBar?: React.ReactNode;
|
||||
/** Toolbar component */
|
||||
toolbar?: React.ReactNode;
|
||||
/** Sidebar/connection panel component */
|
||||
sidebar?: React.ReactNode;
|
||||
/** Main content area component */
|
||||
mainContent?: React.ReactNode;
|
||||
/** Status bar component */
|
||||
statusBar?: React.ReactNode;
|
||||
/** Sidebar collapsed state */
|
||||
sidebarCollapsed?: boolean;
|
||||
/** Handler when sidebar collapse state changes */
|
||||
onSidebarToggle?: (collapsed: boolean) => void;
|
||||
/** Children (alternative to explicit props) */
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* AppLayout - Main application shell component
|
||||
*/
|
||||
export const AppLayout: React.FC<AppLayoutProps> = ({
|
||||
menuBar,
|
||||
toolbar,
|
||||
sidebar,
|
||||
mainContent,
|
||||
statusBar,
|
||||
sidebarCollapsed = false,
|
||||
onSidebarToggle,
|
||||
children,
|
||||
}) => {
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [sidebarWidth, setSidebarWidth] = useState(240);
|
||||
const [isCollapsed, setIsCollapsed] = useState(sidebarCollapsed);
|
||||
|
||||
// Handle sidebar resize start
|
||||
const handleResizeStart = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsResizing(true);
|
||||
}, []);
|
||||
|
||||
// Handle mouse move for resizing
|
||||
useCallback(() => {
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
const newWidth = Math.max(180, Math.min(400, e.clientX));
|
||||
setSidebarWidth(newWidth);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsResizing(false);
|
||||
};
|
||||
|
||||
if (isResizing) {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [isResizing]);
|
||||
|
||||
// Toggle sidebar collapse
|
||||
const toggleSidebar = useCallback(() => {
|
||||
const newCollapsed = !isCollapsed;
|
||||
setIsCollapsed(newCollapsed);
|
||||
onSidebarToggle?.(newCollapsed);
|
||||
}, [isCollapsed, onSidebarToggle]);
|
||||
|
||||
// Expose toggle function via custom event for keyboard shortcuts
|
||||
React.useEffect(() => {
|
||||
const handleToggleRequest = () => toggleSidebar();
|
||||
window.addEventListener('toggle-sidebar', handleToggleRequest);
|
||||
return () => window.removeEventListener('toggle-sidebar', handleToggleRequest);
|
||||
}, [toggleSidebar]);
|
||||
|
||||
return (
|
||||
<div className="app-layout">
|
||||
{/* Menu Bar */}
|
||||
{menuBar && <div className="layout-menubar">{menuBar}</div>}
|
||||
|
||||
{/* Toolbar */}
|
||||
{toolbar && <div className="layout-toolbar">{toolbar}</div>}
|
||||
|
||||
{/* Main content area (sidebar + workspace) */}
|
||||
<div className="layout-main">
|
||||
{/* Sidebar */}
|
||||
{sidebar && (
|
||||
<>
|
||||
<div
|
||||
className={`layout-sidebar ${isCollapsed ? 'collapsed' : ''}`}
|
||||
style={
|
||||
isCollapsed
|
||||
? undefined
|
||||
: { width: sidebarWidth, flex: 'none' }
|
||||
}
|
||||
>
|
||||
{sidebar}
|
||||
</div>
|
||||
|
||||
{/* Resize handle */}
|
||||
{!isCollapsed && (
|
||||
<div
|
||||
className="layout-resize-handle"
|
||||
onMouseDown={handleResizeStart}
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
tabIndex={0}
|
||||
title="Drag to resize sidebar"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Collapse toggle button (when collapsed) */}
|
||||
{isCollapsed && sidebar && (
|
||||
<div className="layout-sidebar-toggle">
|
||||
<button
|
||||
className="btn-icon toggle-button"
|
||||
onClick={toggleSidebar}
|
||||
title="Expand sidebar"
|
||||
aria-label="Expand sidebar"
|
||||
>
|
||||
▶
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Workspace (main content) */}
|
||||
<div className="layout-workspace">
|
||||
{mainContent || children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Bar */}
|
||||
{statusBar && <div className="layout-statusbar">{statusBar}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
120
frontend/src/components/Layout/StatusBar.css
Normal file
120
frontend/src/components/Layout/StatusBar.css
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* StatusBar Component Styles
|
||||
*/
|
||||
|
||||
.statusbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: var(--statusbar-height);
|
||||
padding: 0 var(--space-4);
|
||||
background-color: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border);
|
||||
font-size: var(--text-xs);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Status type variations */
|
||||
.statusbar.status-normal {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.statusbar.status-success {
|
||||
background-color: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.statusbar.status-warning {
|
||||
background-color: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.statusbar.status-error {
|
||||
background-color: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.statusbar.status-info {
|
||||
background-color: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
/* Left section */
|
||||
.statusbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.connection-info {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.query-info {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Right section */
|
||||
.statusbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.status-button {
|
||||
padding: var(--space-1) var(--space-2);
|
||||
font-size: var(--text-xs);
|
||||
font-family: var(--font-sans);
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast) var(--ease-in-out);
|
||||
}
|
||||
|
||||
.status-button:hover {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.editor-mode {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.statusbar {
|
||||
padding: 0 var(--space-2);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.statusbar-left {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.statusbar-right {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.status-button {
|
||||
padding: var(--space-1);
|
||||
}
|
||||
}
|
||||
126
frontend/src/components/Layout/StatusBar.tsx
Normal file
126
frontend/src/components/Layout/StatusBar.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* StatusBar Component
|
||||
*
|
||||
* Bottom status bar showing connection info, encoding, and editor mode.
|
||||
* Based on layout-design.md section "状态栏设计规范"
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import './StatusBar.css';
|
||||
|
||||
export type StatusType = 'normal' | 'success' | 'warning' | 'error' | 'info';
|
||||
|
||||
export interface StatusBarProps {
|
||||
/** Connection status message */
|
||||
connectionInfo?: string;
|
||||
/** Query execution info */
|
||||
queryInfo?: string;
|
||||
/** Encoding format */
|
||||
encoding?: string;
|
||||
/** Line ending type */
|
||||
lineEnding?: 'LF' | 'CRLF';
|
||||
/** Editor mode */
|
||||
editorMode?: 'ins' | 'ovr';
|
||||
/** Status type for coloring */
|
||||
statusType?: StatusType;
|
||||
/** Handler when encoding is clicked */
|
||||
onEncodingClick?: () => void;
|
||||
/** Handler when line ending is clicked */
|
||||
onLineEndingClick?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* StatusBar component
|
||||
*/
|
||||
export const StatusBar: React.FC<StatusBarProps> = ({
|
||||
connectionInfo,
|
||||
queryInfo,
|
||||
encoding = 'UTF-8',
|
||||
lineEnding = 'LF',
|
||||
editorMode = 'ins',
|
||||
statusType = 'normal',
|
||||
onEncodingClick,
|
||||
onLineEndingClick,
|
||||
}) => {
|
||||
// Get status class based on type
|
||||
const getStatusClass = (): string => {
|
||||
switch (statusType) {
|
||||
case 'success':
|
||||
return 'status-success';
|
||||
case 'warning':
|
||||
return 'status-warning';
|
||||
case 'error':
|
||||
return 'status-error';
|
||||
case 'info':
|
||||
return 'status-info';
|
||||
default:
|
||||
return 'status-normal';
|
||||
}
|
||||
};
|
||||
|
||||
// Get status icon
|
||||
const getStatusIcon = (): string => {
|
||||
switch (statusType) {
|
||||
case 'success':
|
||||
return '✓';
|
||||
case 'warning':
|
||||
return '⚠';
|
||||
case 'error':
|
||||
return '✕';
|
||||
case 'info':
|
||||
return 'ℹ';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`statusbar ${getStatusClass()}`}>
|
||||
<div className="statusbar-left">
|
||||
{/* Status icon and message */}
|
||||
{getStatusIcon() && (
|
||||
<span className="status-icon" role="status" aria-label={statusType}>
|
||||
{getStatusIcon()}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Connection info */}
|
||||
{connectionInfo && (
|
||||
<span className="status-item connection-info">{connectionInfo}</span>
|
||||
)}
|
||||
|
||||
{/* Query info (takes priority over connection info when present) */}
|
||||
{queryInfo && (
|
||||
<span className="status-item query-info">{queryInfo}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="statusbar-right">
|
||||
{/* Encoding */}
|
||||
<button
|
||||
className="status-button"
|
||||
onClick={onEncodingClick}
|
||||
title="Change encoding"
|
||||
>
|
||||
{encoding}
|
||||
</button>
|
||||
|
||||
{/* Line ending */}
|
||||
<button
|
||||
className="status-button"
|
||||
onClick={onLineEndingClick}
|
||||
title="Change line ending"
|
||||
>
|
||||
{lineEnding}
|
||||
</button>
|
||||
|
||||
{/* Editor mode */}
|
||||
<span className="status-item editor-mode" title={editorMode === 'ins' ? 'Insert' : 'Overwrite'}>
|
||||
{editorMode}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusBar;
|
||||
124
frontend/src/components/Layout/ToolBar.css
Normal file
124
frontend/src/components/Layout/ToolBar.css
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* ToolBar Component Styles
|
||||
*/
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--toolbar-height);
|
||||
padding: 0 var(--space-4);
|
||||
background-color: var(--bg-primary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
gap: var(--space-4);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-buttons {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.toolbar-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--text-sm);
|
||||
font-family: var(--font-sans);
|
||||
color: var(--text-primary);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast) var(--ease-in-out);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.toolbar-button:hover:not(.disabled) {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.toolbar-button.disabled {
|
||||
color: var(--text-muted);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.button-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.toolbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.toolbar-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Connection indicator */
|
||||
.toolbar-connection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast) var(--ease-in-out);
|
||||
}
|
||||
|
||||
.toolbar-connection:hover {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.connection-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.connection-status-dot.connected {
|
||||
background-color: var(--success);
|
||||
box-shadow: 0 0 4px var(--success);
|
||||
}
|
||||
|
||||
.connection-name {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.toolbar {
|
||||
padding: 0 var(--space-2);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.toolbar-button {
|
||||
padding: var(--space-2);
|
||||
}
|
||||
|
||||
.button-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.connection-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
104
frontend/src/components/Layout/ToolBar.tsx
Normal file
104
frontend/src/components/Layout/ToolBar.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* ToolBar Component
|
||||
*
|
||||
* Quick access toolbar with common actions.
|
||||
* Based on layout-design.md toolbar specifications
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import './ToolBar.css';
|
||||
|
||||
export interface ToolButton {
|
||||
id: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
tooltip?: string;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export interface ToolBarProps {
|
||||
/** Toolbar buttons */
|
||||
buttons?: ToolButton[];
|
||||
/** Additional content (e.g., search box) */
|
||||
children?: React.ReactNode;
|
||||
/** Handler when button is clicked */
|
||||
onButtonClick?: (buttonId: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default toolbar buttons
|
||||
*/
|
||||
const defaultButtons: ToolButton[] = [
|
||||
{
|
||||
id: 'run',
|
||||
icon: '▶',
|
||||
label: 'Run',
|
||||
tooltip: 'Execute query (Ctrl+Enter)',
|
||||
},
|
||||
{
|
||||
id: 'save',
|
||||
icon: '💾',
|
||||
label: 'Save',
|
||||
tooltip: 'Save query (Ctrl+S)',
|
||||
},
|
||||
{
|
||||
id: 'export',
|
||||
icon: '📤',
|
||||
label: 'Export',
|
||||
tooltip: 'Export results',
|
||||
},
|
||||
{
|
||||
id: 'find',
|
||||
icon: '🔍',
|
||||
label: 'Find',
|
||||
tooltip: 'Find in query (Ctrl+F)',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* ToolBar component
|
||||
*/
|
||||
export const ToolBar: React.FC<ToolBarProps> = ({
|
||||
buttons = defaultButtons,
|
||||
children,
|
||||
onButtonClick,
|
||||
}) => {
|
||||
const handleButtonClick = (button: ToolButton) => {
|
||||
button.onClick?.();
|
||||
onButtonClick?.(button.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="toolbar" role="toolbar" aria-label="Main toolbar">
|
||||
<div className="toolbar-buttons">
|
||||
{buttons.map((button) => (
|
||||
<button
|
||||
key={button.id}
|
||||
className={`toolbar-button ${button.disabled ? 'disabled' : ''}`}
|
||||
onClick={() => handleButtonClick(button)}
|
||||
disabled={button.disabled}
|
||||
title={button.tooltip}
|
||||
aria-label={button.label}
|
||||
>
|
||||
<span className="button-icon">{button.icon}</span>
|
||||
<span className="button-label">{button.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{children && <div className="toolbar-content">{children}</div>}
|
||||
|
||||
<div className="toolbar-spacer" />
|
||||
|
||||
{/* Connection indicator */}
|
||||
<div className="toolbar-connection">
|
||||
<span className="connection-status-dot connected"></span>
|
||||
<span className="connection-name">🗄️ MySQL @ localhost</span>
|
||||
<span className="dropdown-arrow">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolBar;
|
||||
Reference in New Issue
Block a user