diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index c00bcc79..7d469f53 100644 --- a/.flutter-plugins-dependencies +++ b/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"path_provider_foundation","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"jni","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni_flutter","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/jni_flutter-1.0.1/","native_build":true,"dependencies":["jni"],"dev_dependency":false},{"name":"path_provider_android","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_android-2.3.1/","native_build":false,"dependencies":["jni","jni_flutter"],"dev_dependency":false},{"name":"sqflite_android","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/sqflite_android-2.4.2+3/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"bitsdojo_window_macos","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/bitsdojo_window_macos-0.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"bitsdojo_window_linux","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/bitsdojo_window_linux-0.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"bitsdojo_window_windows","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/bitsdojo_window_windows-0.1.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[]},"dependencyGraph":[{"name":"bitsdojo_window","dependencies":["bitsdojo_window_windows","bitsdojo_window_macos","bitsdojo_window_linux"]},{"name":"bitsdojo_window_linux","dependencies":[]},{"name":"bitsdojo_window_macos","dependencies":[]},{"name":"bitsdojo_window_windows","dependencies":[]},{"name":"jni","dependencies":[]},{"name":"jni_flutter","dependencies":["jni"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":["jni","jni_flutter"]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]}],"date_created":"2026-04-11 12:30:47.798124","version":"3.41.6","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"path_provider_foundation","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"jni","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni_flutter","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/jni_flutter-1.0.1/","native_build":true,"dependencies":["jni"],"dev_dependency":false},{"name":"path_provider_android","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_android-2.3.1/","native_build":false,"dependencies":["jni","jni_flutter"],"dev_dependency":false},{"name":"sqflite_android","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/sqflite_android-2.4.2+3/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"bitsdojo_window_macos","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/bitsdojo_window_macos-0.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"bitsdojo_window_linux","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/bitsdojo_window_linux-0.1.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"bitsdojo_window_windows","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/bitsdojo_window_windows-0.1.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/zhuchka/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[]},"dependencyGraph":[{"name":"bitsdojo_window","dependencies":["bitsdojo_window_windows","bitsdojo_window_macos","bitsdojo_window_linux"]},{"name":"bitsdojo_window_linux","dependencies":[]},{"name":"bitsdojo_window_macos","dependencies":[]},{"name":"bitsdojo_window_windows","dependencies":[]},{"name":"jni","dependencies":[]},{"name":"jni_flutter","dependencies":["jni"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":["jni","jni_flutter"]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]}],"date_created":"2026-04-19 12:51:07.216547","version":"3.41.6","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5c073f8c..229ea50b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ Thumbs.db *.dll *.exe flutter_*.log +design-front diff --git a/lib/core/editor/sql_syntax_highlighting.dart b/lib/core/editor/sql_syntax_highlighting.dart new file mode 100644 index 00000000..5dfb2276 --- /dev/null +++ b/lib/core/editor/sql_syntax_highlighting.dart @@ -0,0 +1,7 @@ +/// Future work: editable SQL with syntax highlighting (see plan: syntax-highlight-epic). +/// +/// Candidates: custom [EditableText] + [TextPainter], or a dedicated code-editor package. +/// Plain [TextField] remains the source of truth until an editor is chosen. +abstract class SqlSyntaxHighlighting { + const SqlSyntaxHighlighting._(); +} diff --git a/lib/core/layout/window_layout.dart b/lib/core/layout/window_layout.dart new file mode 100644 index 00000000..a926d607 --- /dev/null +++ b/lib/core/layout/window_layout.dart @@ -0,0 +1,81 @@ +import 'dart:math' as math; + +import 'package:flutter/widgets.dart'; + +/// Breakpoints and sizes derived from window / overlay size (desktop adaptive UI). +abstract class WindowLayout { + WindowLayout._(); + + static const double narrowWindowWidth = 720; + static const double compactWindowWidth = 520; + + /// Horizontal inset for modal dialogs (clamped by screen). + static double dialogHorizontalInset(double screenWidth) { + return (screenWidth * 0.05).clamp(12.0, 48.0); + } + + static double dialogVerticalInset(double screenHeight) { + return (screenHeight * 0.04).clamp(12.0, 40.0); + } + + /// Use for [Dialog.insetPadding] / modal margins on small windows. + static EdgeInsets dialogSymmetricInsets(BuildContext context) { + final mq = MediaQuery.sizeOf(context); + return EdgeInsets.symmetric( + horizontal: dialogHorizontalInset(mq.width), + vertical: dialogVerticalInset(mq.height), + ); + } + + /// "Select database" and similar pickers. + static double newConnectionDialogMaxWidth(double screenWidth) { + final inset = dialogHorizontalInset(screenWidth) * 2; + return math.min(740, math.max(280.0, screenWidth - inset)); + } + + static double newConnectionDialogHeight(double screenHeight) { + final inset = dialogVerticalInset(screenHeight) * 2; + final h = screenHeight - inset; + return math.min(580, math.max(320.0, h * 0.78)); + } + + static double newConnectionSidebarWidth(double dialogWidth) { + if (dialogWidth < 400) return 88; + if (dialogWidth < 520) return 108; + if (dialogWidth < 640) return 124; + return 140; + } + + /// Grid area width (right of sidebar, inner padding applied separately). + static int dbTypeGridCrossAxisCount(double gridInnerWidth) { + if (gridInnerWidth >= 460) return 4; + if (gridInnerWidth >= 240) return 2; + return 1; + } + + static double dbTypeCardHeight(int crossAxisCount) { + return switch (crossAxisCount) { + 4 => 144, + 2 => 138, + _ => 132, + }; + } + + /// Empty workspace hero content max width (stays within viewport minus padding). + static double heroContentMaxWidth(double viewportWidth) { + final pad = heroHorizontalPadding(viewportWidth) * 2; + final inner = math.max(0.0, viewportWidth - pad); + return math.min(560, inner); + } + + static double heroHorizontalPadding(double viewportWidth) { + if (viewportWidth < compactWindowWidth) return 16; + if (viewportWidth < narrowWindowWidth) return 22; + return 28; + } + + /// Mock window block height scales with available width. + static double heroMockWindowHeight(double contentWidth) { + return (contentWidth * 0.38).clamp(140.0, 220.0); + } +} diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index e66c1385..4e8fe381 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -1,10 +1,12 @@ import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'querya_color_scheme.dart'; + /// App theme: dark only. Used for both theme and darkTheme so the app is always dark. abstract class AppTheme { static ThemeData get dark => const ThemeData.dark( - colorScheme: ColorSchemes.darkSlate, - radius: 0.5, + colorScheme: QueryaColorScheme.dark, + radius: 0.58, scaling: 1, typography: Typography.geist(), ); diff --git a/lib/core/theme/querya_color_scheme.dart b/lib/core/theme/querya_color_scheme.dart new file mode 100644 index 00000000..f57e203b --- /dev/null +++ b/lib/core/theme/querya_color_scheme.dart @@ -0,0 +1,34 @@ +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +import 'querya_colors.dart'; + +/// Dark [ColorScheme] for Querya desktop — matches [QueryaColors] tokens. +abstract class QueryaColorScheme { + static const ColorScheme dark = ColorScheme( + brightness: Brightness.dark, + background: QueryaColors.canvas, + foreground: Color(0xFFF8FAFC), + card: QueryaColors.surface, + cardForeground: Color(0xFFF8FAFC), + popover: QueryaColors.surface, + popoverForeground: Color(0xFFF8FAFC), + primary: QueryaColors.accentCyan, + primaryForeground: QueryaColors.onAccent, + secondary: Color(0xFF18181B), + secondaryForeground: Color(0xFFF8FAFC), + muted: Color(0xFF18181B), + mutedForeground: QueryaColors.mutedLabel, + accent: Color(0xFF27272A), + accentForeground: Color(0xFFF8FAFC), + destructive: Color(0xFFEF4444), + destructiveForeground: Color(0xFFF8FAFC), + border: QueryaColors.borderSubtle, + input: QueryaColors.borderSubtle, + ring: QueryaColors.accentCyan, + chart1: Color(0xFF2662D9), + chart2: Color(0xFF2EB88A), + chart3: Color(0xFFE88C30), + chart4: Color(0xFFAF57DB), + chart5: Color(0xFFE23670), + ); +} diff --git a/lib/core/theme/querya_colors.dart b/lib/core/theme/querya_colors.dart new file mode 100644 index 00000000..af0707d0 --- /dev/null +++ b/lib/core/theme/querya_colors.dart @@ -0,0 +1,27 @@ +import 'dart:ui'; + +/// Design tokens aligned with [design-front] mockups (near-black canvas, cyan accent). +/// +/// Canonical semantic mapping lives in [QueryaColorScheme]; these are named aliases +/// for documentation and for a few widgets that need the raw accent (e.g. glows). +abstract class QueryaColors { + QueryaColors._(); + + /// Main app background — pure black on reference hero. + static const Color canvas = Color(0xFF000000); + + /// Elevated surface (cards, mock window chrome). + static const Color surface = Color(0xFF0C0C0C); + + /// Brand accent (CTAs, tree icons, focus ring). + static const Color accentCyan = Color(0xFF22D3EE); + + /// Text / icons on filled primary buttons. + static const Color onAccent = Color(0xFF0A0A0A); + + /// Hairline borders on dark UI. + static const Color borderSubtle = Color(0xFF27272A); + + /// Secondary label tone (body muted on mockups). + static const Color mutedLabel = Color(0xFF94A3B8); +} diff --git a/lib/core/theme/querya_typography.dart b/lib/core/theme/querya_typography.dart new file mode 100644 index 00000000..4b5faf39 --- /dev/null +++ b/lib/core/theme/querya_typography.dart @@ -0,0 +1,6 @@ +/// Monospace stack for SQL editors (bundled font can replace this later). +abstract class QueryaTypography { + QueryaTypography._(); + + static const String mono = 'monospace'; +} diff --git a/lib/features/connections/connection_creation_flow.dart b/lib/features/connections/connection_creation_flow.dart new file mode 100644 index 00000000..935ff472 --- /dev/null +++ b/lib/features/connections/connection_creation_flow.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart' as material; + +import 'package:querya_desktop/core/storage/local_db.dart'; +import 'package:querya_desktop/features/connections/new_connection_dialog.dart'; +import 'package:querya_desktop/features/mongodb/mongodb_connection_form.dart'; +import 'package:querya_desktop/features/mysql/mysql_connection_form.dart'; +import 'package:querya_desktop/features/postgresql/postgresql_connection_form.dart'; +import 'package:querya_desktop/features/redis/redis_connection_form.dart'; + +/// Picks a database type, opens the matching form, returns a saved row or null. +Future promptCreateConnection( + material.BuildContext context, { + int? folderId, +}) async { + final type = await showNewConnectionDialog(context); + if (type == null) return null; + if (!context.mounted) return null; + switch (type) { + case ConnectionType.postgresql: + return await showPostgresConnectionForm(context, folderId: folderId); + case ConnectionType.mysql: + return await showMysqlConnectionForm(context, folderId: folderId); + case ConnectionType.mongodb: + return await showMongoConnectionForm(context, folderId: folderId); + case ConnectionType.redis: + return await showRedisConnectionForm(context, folderId: folderId); + } +} diff --git a/lib/features/connections/connections_panel.dart b/lib/features/connections/connections_panel.dart index 5ae139f8..10ad02c2 100644 --- a/lib/features/connections/connections_panel.dart +++ b/lib/features/connections/connections_panel.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart' as material show AlertDialog, BoxConstraints, BuildContext, Column, ConstrainedBox, Container, BoxDecoration, Border, BorderSide, InkWell, Icon, Icons, IconData, Image, EdgeInsets, BorderRadius, CrossAxisAlignment, MainAxisSize, MouseRegion, SystemMouseCursors, DefaultTextStyle, TextStyle, CustomScrollView, SliverToBoxAdapter, SliverFillRemaining, SliverPadding, GestureDetector, HitTestBehavior, SizedBox, AnimatedRotation, Row, BoxFit, Text, TextOverflow, Expanded, CircularProgressIndicator, Material, StatelessWidget, Colors, Tooltip, Color, LayoutBuilder, TextPainter, TextSpan, TextDirection, SelectableText, Padding, Widget, Navigator, ValueKey; +import 'package:flutter/material.dart' as material show AlertDialog, BoxConstraints, BuildContext, Column, ConstrainedBox, Container, BoxDecoration, Border, BorderSide, InkWell, Icon, Icons, IconData, Image, EdgeInsets, BorderRadius, CrossAxisAlignment, MainAxisSize, MouseRegion, SystemMouseCursors, TextStyle, CustomScrollView, SliverToBoxAdapter, SliverFillRemaining, SliverPadding, GestureDetector, HitTestBehavior, SizedBox, AnimatedRotation, Row, BoxFit, Text, TextOverflow, Expanded, CircularProgressIndicator, Material, StatelessWidget, Colors, Tooltip, Color, LayoutBuilder, TextPainter, TextSpan, TextDirection, SelectableText, Padding, Widget, Navigator, ValueKey, FontWeight, VoidCallback; import 'package:flutter/services.dart' show Clipboard, ClipboardData; import 'package:querya_desktop/core/database/mongodb_service.dart'; import 'package:querya_desktop/core/database/mysql_service.dart'; @@ -7,22 +7,49 @@ import 'package:querya_desktop/core/database/redis_connection.dart'; import 'package:querya_desktop/core/database/redis_info.dart'; import 'package:querya_desktop/core/storage/folders_storage.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; +import 'package:querya_desktop/core/theme/querya_typography.dart'; +import 'package:querya_desktop/features/connections/connection_creation_flow.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; import 'package:querya_desktop/features/mongodb/mongo_database_dialog.dart'; -import 'package:querya_desktop/features/mongodb/mongodb_connection_form.dart'; import 'package:querya_desktop/features/postgresql/postgres_object_kind.dart'; -import 'package:querya_desktop/features/mysql/mysql_connection_form.dart'; import 'package:querya_desktop/features/mysql/mysql_object_kind.dart'; -import 'package:querya_desktop/features/postgresql/postgresql_connection_form.dart'; -import 'package:querya_desktop/features/redis/redis_connection_form.dart'; -import 'new_connection_dialog.dart'; import 'new_folder_dialog.dart'; +material.Widget _sidebarConnectionShell({ + required material.BuildContext context, + required bool isSelected, + required material.VoidCallback? onTap, + required material.Widget child, +}) { + final p = Theme.of(context).colorScheme.primary; + return material.Material( + color: material.Colors.transparent, + child: material.InkWell( + onTap: onTap, + borderRadius: material.BorderRadius.circular(20), + mouseCursor: onTap != null + ? material.SystemMouseCursors.click + : material.SystemMouseCursors.basic, + child: material.Container( + decoration: material.BoxDecoration( + color: isSelected ? p.withValues(alpha: 0.16) : null, + borderRadius: material.BorderRadius.circular(20), + border: isSelected + ? material.Border.all(color: p.withValues(alpha: 0.26)) + : null, + ), + child: child, + ), + ), + ); +} + /// Left panel: Browser tree (pgAdmin-style). Uses shadcn layout widgets. class ConnectionsPanel extends StatefulWidget { const ConnectionsPanel({ super.key, + this.selectedConnectionId, this.onConnectionSelected, this.onRedisDatabaseSelected, this.onMongoDBDatabaseSelected, @@ -37,6 +64,9 @@ class ConnectionsPanel extends StatefulWidget { this.skipInitialDbLoadForTest = false, }); + /// Highlights the active connection row in the sidebar (workspace selection). + final int? selectedConnectionId; + /// Called when the user taps a connection tile. final void Function(ConnectionRow connection)? onConnectionSelected; @@ -147,21 +177,14 @@ class ConnectionsPanelState extends State { if (mounted) setState(() => _folders = FoldersStorage.instance.folders); } - Future _createConnection(ConnectionType type, {int? folderId}) async { - ConnectionRow? row; - - if (type == ConnectionType.postgresql) { - row = await showPostgresConnectionForm(context, folderId: folderId); - } else if (type == ConnectionType.mysql) { - row = await showMysqlConnectionForm(context, folderId: folderId); - } else if (type == ConnectionType.mongodb) { - row = await showMongoConnectionForm(context, folderId: folderId); - } else if (type == ConnectionType.redis) { - row = await showRedisConnectionForm(context, folderId: folderId); - } else { - row = null; - } - + Future _createConnection({ + int? folderId, + material.BuildContext? dialogContext, + }) async { + final row = await promptCreateConnection( + dialogContext ?? context, + folderId: folderId, + ); if (row != null && mounted) { await LocalDb.instance.addConnection(row); await _loadData(); @@ -197,9 +220,12 @@ class ConnectionsPanelState extends State { } Widget _buildConnectionTile(ConnectionRow conn) { + final isSelected = + widget.selectedConnectionId != null && widget.selectedConnectionId == conn.id; if (conn.type == 'postgresql') { return _PostgresConnectionTile( connection: conn, + isSelected: isSelected, icon: _iconForType(conn.type), iconAsset: _iconAssetForType(conn.type), onRemove: () => _removeConnection(conn.id!), @@ -210,6 +236,7 @@ class ConnectionsPanelState extends State { } else if (conn.type == 'mysql') { return _MysqlConnectionTile( connection: conn, + isSelected: isSelected, icon: _iconForType(conn.type), iconAsset: _iconAssetForType(conn.type), onRemove: () => _removeConnection(conn.id!), @@ -220,6 +247,7 @@ class ConnectionsPanelState extends State { } else if (conn.type == 'redis') { return _RedisConnectionTile( connection: conn, + isSelected: isSelected, icon: _iconForType(conn.type), iconAsset: _iconAssetForType(conn.type), onRemove: () => _removeConnection(conn.id!), @@ -229,6 +257,7 @@ class ConnectionsPanelState extends State { } else if (conn.type == 'mongodb') { return _MongoConnectionTile( connection: conn, + isSelected: isSelected, icon: _iconForType(conn.type), iconAsset: _iconAssetForType(conn.type), onRemove: () => _removeConnection(conn.id!), @@ -238,6 +267,7 @@ class ConnectionsPanelState extends State { } return _ConnectionTile( connection: conn, + isSelected: isSelected, icon: _iconForType(conn.type), iconAsset: _iconAssetForType(conn.type), onRemove: () => _removeConnection(conn.id!), @@ -257,7 +287,7 @@ class ConnectionsPanelState extends State { color: theme.colorScheme.background, border: material.Border( right: material.BorderSide( - color: theme.colorScheme.border.withValues(alpha: 0.4), + color: theme.colorScheme.border.withValues(alpha: 0.28), width: 1, ), ), @@ -267,12 +297,18 @@ class ConnectionsPanelState extends State { children: [ material.Padding( padding: const material.EdgeInsets.fromLTRB(20, 24, 16, 16), - child: material.DefaultTextStyle( - style: material.TextStyle(color: theme.colorScheme.mutedForeground), - child: const Text('Browser').semiBold().small(), + child: material.Text( + 'SERVERS', + style: material.TextStyle( + fontFamily: QueryaTypography.mono, + fontSize: 11, + letterSpacing: 0.85, + fontWeight: material.FontWeight.w600, + color: theme.colorScheme.mutedForeground, + ), ), ), - Divider(height: 1, color: theme.colorScheme.border.withValues(alpha: 0.3)), + Divider(height: 1, color: theme.colorScheme.border.withValues(alpha: 0.22)), Expanded( child: material.CustomScrollView( slivers: [ @@ -305,10 +341,9 @@ class ConnectionsPanelState extends State { await _loadData(); }, onNewConnection: (folderName) async { - final type = await showNewConnectionDialog(context); - if (type == null || !mounted) return; - final folderId = await LocalDb.instance.getFolderIdByName(folderName); - await _createConnection(type, folderId: folderId); + final folderId = + await LocalDb.instance.getFolderIdByName(folderName); + await _createConnection(folderId: folderId); }, iconForType: _iconForType, onRemoveConnection: _removeConnection, @@ -340,11 +375,9 @@ class ConnectionsPanelState extends State { MenuButton( leading: material.Icon(material.Icons.settings_ethernet_rounded, size: 18, color: theme.colorScheme.mutedForeground), onPressed: (menuContext) async { - final type = await showNewConnectionDialog(menuContext); - if (type == null || !mounted) return; await Future.delayed(const Duration(milliseconds: 100)); - if (!mounted) return; - await _createConnection(type); + if (!mounted || !menuContext.mounted) return; + await _createConnection(dialogContext: menuContext); }, child: const Text('New Connection'), ), @@ -412,6 +445,7 @@ class _EmptyState extends StatelessWidget { class _ConnectionTile extends StatelessWidget { const _ConnectionTile({ required this.connection, + this.isSelected = false, required this.icon, this.iconAsset, required this.onRemove, @@ -419,6 +453,7 @@ class _ConnectionTile extends StatelessWidget { }); final ConnectionRow connection; + final bool isSelected; final material.IconData icon; final String? iconAsset; final VoidCallback onRemove; @@ -450,46 +485,45 @@ class _ConnectionTile extends StatelessWidget { ], child: material.Padding( padding: const material.EdgeInsets.only(bottom: 2), - child: material.MouseRegion( - cursor: material.SystemMouseCursors.click, - child: material.InkWell( - onTap: onTap, - borderRadius: material.BorderRadius.circular(6), - child: material.Padding( - padding: const material.EdgeInsets.symmetric(horizontal: 8, vertical: 6), - child: material.Row( - children: [ - iconWidget, - const Gap(8), - material.Expanded( - child: material.Column( - crossAxisAlignment: material.CrossAxisAlignment.start, - mainAxisSize: material.MainAxisSize.min, - children: [ + child: _sidebarConnectionShell( + context: context, + isSelected: isSelected, + onTap: onTap, + child: material.Padding( + padding: + const material.EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: material.Row( + children: [ + iconWidget, + const Gap(8), + material.Expanded( + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + mainAxisSize: material.MainAxisSize.min, + children: [ + material.Text( + connection.name, + overflow: material.TextOverflow.ellipsis, + maxLines: 1, + style: material.TextStyle( + fontSize: 13, + color: theme.colorScheme.foreground, + ), + ), + if (connection.host != null) material.Text( - connection.name, + '${connection.host}:${connection.port ?? ''}', overflow: material.TextOverflow.ellipsis, maxLines: 1, style: material.TextStyle( - fontSize: 13, - color: theme.colorScheme.foreground, + fontSize: 11, + color: theme.colorScheme.mutedForeground, ), ), - if (connection.host != null) - material.Text( - '${connection.host}:${connection.port ?? ''}', - overflow: material.TextOverflow.ellipsis, - maxLines: 1, - style: material.TextStyle( - fontSize: 11, - color: theme.colorScheme.mutedForeground, - ), - ), - ], - ), + ], ), - ], - ), + ), + ], ), ), ), @@ -613,6 +647,7 @@ class _FolderTile extends StatelessWidget { class _RedisConnectionTile extends StatefulWidget { const _RedisConnectionTile({ required this.connection, + this.isSelected = false, required this.icon, this.iconAsset, required this.onRemove, @@ -621,6 +656,7 @@ class _RedisConnectionTile extends StatefulWidget { }); final ConnectionRow connection; + final bool isSelected; final material.IconData icon; final String? iconAsset; final VoidCallback onRemove; @@ -766,48 +802,46 @@ class _RedisConnectionTileState extends State<_RedisConnectionTile> { ), // Connection name — clickable for stats material.Expanded( - child: material.MouseRegion( - cursor: material.SystemMouseCursors.click, - child: material.InkWell( - onTap: widget.onTap, - borderRadius: material.BorderRadius.circular(6), - child: material.Padding( - padding: const material.EdgeInsets.symmetric( - horizontal: 4, vertical: 6), - child: material.Row( - children: [ - iconWidget, - const Gap(8), - material.Expanded( - child: material.Column( - crossAxisAlignment: - material.CrossAxisAlignment.start, - mainAxisSize: material.MainAxisSize.min, - children: [ + child: _sidebarConnectionShell( + context: context, + isSelected: widget.isSelected, + onTap: widget.onTap, + child: material.Padding( + padding: const material.EdgeInsets.symmetric( + horizontal: 4, vertical: 6), + child: material.Row( + children: [ + iconWidget, + const Gap(8), + material.Expanded( + child: material.Column( + crossAxisAlignment: + material.CrossAxisAlignment.start, + mainAxisSize: material.MainAxisSize.min, + children: [ + material.Text( + widget.connection.name, + overflow: material.TextOverflow.ellipsis, + maxLines: 1, + style: material.TextStyle( + fontSize: 13, + color: theme.colorScheme.foreground, + ), + ), + if (widget.connection.host != null) material.Text( - widget.connection.name, + '${widget.connection.host}:${widget.connection.port ?? ''}', overflow: material.TextOverflow.ellipsis, maxLines: 1, style: material.TextStyle( - fontSize: 13, - color: theme.colorScheme.foreground, + fontSize: 11, + color: theme.colorScheme.mutedForeground, ), ), - if (widget.connection.host != null) - material.Text( - '${widget.connection.host}:${widget.connection.port ?? ''}', - overflow: material.TextOverflow.ellipsis, - maxLines: 1, - style: material.TextStyle( - fontSize: 11, - color: theme.colorScheme.mutedForeground, - ), - ), - ], - ), + ], ), - ], - ), + ), + ], ), ), ), @@ -924,6 +958,7 @@ class _RedisDatabaseNode extends StatelessWidget { class _MongoConnectionTile extends StatefulWidget { const _MongoConnectionTile({ required this.connection, + this.isSelected = false, required this.icon, this.iconAsset, required this.onRemove, @@ -932,6 +967,7 @@ class _MongoConnectionTile extends StatefulWidget { }); final ConnectionRow connection; + final bool isSelected; final material.IconData icon; final String? iconAsset; final VoidCallback onRemove; @@ -1098,48 +1134,46 @@ class _MongoConnectionTileState extends State<_MongoConnectionTile> { ), // Connection name — clickable for stats material.Expanded( - child: material.MouseRegion( - cursor: material.SystemMouseCursors.click, - child: material.InkWell( - onTap: widget.onTap, - borderRadius: material.BorderRadius.circular(6), - child: material.Padding( - padding: const material.EdgeInsets.symmetric( - horizontal: 4, vertical: 6), - child: material.Row( - children: [ - iconWidget, - const Gap(8), - material.Expanded( - child: material.Column( - crossAxisAlignment: - material.CrossAxisAlignment.start, - mainAxisSize: material.MainAxisSize.min, - children: [ + child: _sidebarConnectionShell( + context: context, + isSelected: widget.isSelected, + onTap: widget.onTap, + child: material.Padding( + padding: const material.EdgeInsets.symmetric( + horizontal: 4, vertical: 6), + child: material.Row( + children: [ + iconWidget, + const Gap(8), + material.Expanded( + child: material.Column( + crossAxisAlignment: + material.CrossAxisAlignment.start, + mainAxisSize: material.MainAxisSize.min, + children: [ + material.Text( + widget.connection.name, + overflow: material.TextOverflow.ellipsis, + maxLines: 1, + style: material.TextStyle( + fontSize: 13, + color: theme.colorScheme.foreground, + ), + ), + if (widget.connection.host != null) material.Text( - widget.connection.name, + '${widget.connection.host}:${widget.connection.port ?? ''}', overflow: material.TextOverflow.ellipsis, maxLines: 1, style: material.TextStyle( - fontSize: 13, - color: theme.colorScheme.foreground, + fontSize: 11, + color: theme.colorScheme.mutedForeground, ), ), - if (widget.connection.host != null) - material.Text( - '${widget.connection.host}:${widget.connection.port ?? ''}', - overflow: material.TextOverflow.ellipsis, - maxLines: 1, - style: material.TextStyle( - fontSize: 11, - color: theme.colorScheme.mutedForeground, - ), - ), - ], - ), + ], ), - ], - ), + ), + ], ), ), ), @@ -1272,6 +1306,7 @@ class _MongoDatabaseNode extends StatelessWidget { class _PostgresConnectionTile extends StatefulWidget { const _PostgresConnectionTile({ required this.connection, + this.isSelected = false, required this.icon, this.iconAsset, required this.onRemove, @@ -1281,6 +1316,7 @@ class _PostgresConnectionTile extends StatefulWidget { }); final ConnectionRow connection; + final bool isSelected; final material.IconData icon; final String? iconAsset; final VoidCallback onRemove; @@ -1407,48 +1443,46 @@ class _PostgresConnectionTileState extends State<_PostgresConnectionTile> { ), ), material.Expanded( - child: material.MouseRegion( - cursor: material.SystemMouseCursors.click, - child: material.InkWell( - onTap: widget.onTap, - borderRadius: material.BorderRadius.circular(6), - child: material.Padding( - padding: const material.EdgeInsets.symmetric( - horizontal: 4, vertical: 6), - child: material.Row( - children: [ - iconWidget, - const Gap(8), - material.Expanded( - child: material.Column( - crossAxisAlignment: - material.CrossAxisAlignment.start, - mainAxisSize: material.MainAxisSize.min, - children: [ + child: _sidebarConnectionShell( + context: context, + isSelected: widget.isSelected, + onTap: widget.onTap, + child: material.Padding( + padding: const material.EdgeInsets.symmetric( + horizontal: 4, vertical: 6), + child: material.Row( + children: [ + iconWidget, + const Gap(8), + material.Expanded( + child: material.Column( + crossAxisAlignment: + material.CrossAxisAlignment.start, + mainAxisSize: material.MainAxisSize.min, + children: [ + material.Text( + widget.connection.name, + overflow: material.TextOverflow.ellipsis, + maxLines: 1, + style: material.TextStyle( + fontSize: 13, + color: theme.colorScheme.foreground, + ), + ), + if (widget.connection.host != null) material.Text( - widget.connection.name, + '${widget.connection.host}:${widget.connection.port ?? ''}', overflow: material.TextOverflow.ellipsis, maxLines: 1, style: material.TextStyle( - fontSize: 13, - color: theme.colorScheme.foreground, + fontSize: 11, + color: theme.colorScheme.mutedForeground, ), ), - if (widget.connection.host != null) - material.Text( - '${widget.connection.host}:${widget.connection.port ?? ''}', - overflow: material.TextOverflow.ellipsis, - maxLines: 1, - style: material.TextStyle( - fontSize: 11, - color: theme.colorScheme.mutedForeground, - ), - ), - ], - ), + ], ), - ], - ), + ), + ], ), ), ), @@ -1509,6 +1543,7 @@ class _PostgresConnectionTileState extends State<_PostgresConnectionTile> { class _MysqlConnectionTile extends StatefulWidget { const _MysqlConnectionTile({ required this.connection, + this.isSelected = false, required this.icon, this.iconAsset, required this.onRemove, @@ -1518,6 +1553,7 @@ class _MysqlConnectionTile extends StatefulWidget { }); final ConnectionRow connection; + final bool isSelected; final material.IconData icon; final String? iconAsset; final VoidCallback onRemove; @@ -1648,49 +1684,47 @@ class _MysqlConnectionTileState extends State<_MysqlConnectionTile> { ), ), material.Expanded( - child: material.MouseRegion( - cursor: material.SystemMouseCursors.click, - child: material.InkWell( - onTap: widget.onTap, - borderRadius: material.BorderRadius.circular(6), - child: material.Padding( - padding: const material.EdgeInsets.symmetric( - horizontal: 4, vertical: 6), - child: material.Row( - children: [ - iconWidget, - const Gap(8), - material.Expanded( - child: material.Column( - crossAxisAlignment: - material.CrossAxisAlignment.start, - mainAxisSize: material.MainAxisSize.min, - children: [ + child: _sidebarConnectionShell( + context: context, + isSelected: widget.isSelected, + onTap: widget.onTap, + child: material.Padding( + padding: const material.EdgeInsets.symmetric( + horizontal: 4, vertical: 6), + child: material.Row( + children: [ + iconWidget, + const Gap(8), + material.Expanded( + child: material.Column( + crossAxisAlignment: + material.CrossAxisAlignment.start, + mainAxisSize: material.MainAxisSize.min, + children: [ + material.Text( + widget.connection.name, + overflow: material.TextOverflow.ellipsis, + maxLines: 1, + style: material.TextStyle( + fontSize: 13, + color: theme.colorScheme.foreground, + ), + ), + if (widget.connection.host != null) material.Text( - widget.connection.name, + '${widget.connection.host}:${widget.connection.port ?? ''}', overflow: material.TextOverflow.ellipsis, maxLines: 1, style: material.TextStyle( - fontSize: 13, - color: theme.colorScheme.foreground, + fontSize: 11, + color: + theme.colorScheme.mutedForeground, ), ), - if (widget.connection.host != null) - material.Text( - '${widget.connection.host}:${widget.connection.port ?? ''}', - overflow: material.TextOverflow.ellipsis, - maxLines: 1, - style: material.TextStyle( - fontSize: 11, - color: - theme.colorScheme.mutedForeground, - ), - ), - ], - ), + ], ), - ], - ), + ), + ], ), ), ), diff --git a/lib/features/connections/driver_manager_dialog.dart b/lib/features/connections/driver_manager_dialog.dart index dc3ece3c..fa386c67 100644 --- a/lib/features/connections/driver_manager_dialog.dart +++ b/lib/features/connections/driver_manager_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart' as material; import 'package:querya_desktop/core/drivers/driver_storage.dart'; +import 'package:querya_desktop/core/layout/window_layout.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; import 'new_connection_dialog.dart'; @@ -30,10 +31,10 @@ final _driverInfoList = <_DriverInfo>[ void showDriverManagerDialog(BuildContext context) { showAppDialog( context: context, - builder: (context) => const material.Dialog( + builder: (context) => material.Dialog( backgroundColor: material.Colors.transparent, - insetPadding: material.EdgeInsets.symmetric(horizontal: 40, vertical: 24), - child: _DriverManagerDialogContent(), + insetPadding: WindowLayout.dialogSymmetricInsets(context), + child: const _DriverManagerDialogContent(), ), ); } diff --git a/lib/features/connections/new_connection_dialog.dart b/lib/features/connections/new_connection_dialog.dart index cf940bc0..62b9bb61 100644 --- a/lib/features/connections/new_connection_dialog.dart +++ b/lib/features/connections/new_connection_dialog.dart @@ -1,4 +1,7 @@ +import 'dart:math' as math; + import 'package:flutter/material.dart' as material; +import 'package:querya_desktop/core/layout/window_layout.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; /// Database type for new connection. @@ -43,10 +46,10 @@ enum _Category { all, sql, nosql } Future showNewConnectionDialog(BuildContext context) { return showAppDialog( context: context, - builder: (context) => const material.Dialog( + builder: (context) => material.Dialog( backgroundColor: material.Colors.transparent, - insetPadding: material.EdgeInsets.symmetric(horizontal: 40, vertical: 24), - child: _NewConnectionDialogContent(), + insetPadding: WindowLayout.dialogSymmetricInsets(context), + child: const _NewConnectionDialogContent(), ), ); } @@ -86,25 +89,34 @@ class _NewConnectionDialogContentState extends material.State<_NewConnectionDial material.Widget build(material.BuildContext context) { final theme = Theme.of(context).colorScheme; final radius = Theme.of(context).radiusXxl; + final mq = MediaQuery.sizeOf(context); + final dialogMaxW = WindowLayout.newConnectionDialogMaxWidth(mq.width); + final dialogH = WindowLayout.newConnectionDialogHeight(mq.height); + final sidebarW = WindowLayout.newConnectionSidebarWidth(dialogMaxW); + final headerPadH = dialogMaxW < 420 ? 16.0 : 24.0; + return material.Container( - constraints: const material.BoxConstraints(maxWidth: 740, minHeight: 380), + width: dialogMaxW, + constraints: material.BoxConstraints( + maxWidth: dialogMaxW, + maxHeight: dialogH, + minHeight: math.min(320.0, dialogH), + ), decoration: material.BoxDecoration( color: theme.popover, borderRadius: material.BorderRadius.circular(radius), border: material.Border.all(color: theme.muted), ), - child: material.ConstrainedBox( - constraints: const material.BoxConstraints(maxWidth: 740, minHeight: 380), - child: material.ClipRRect( - borderRadius: material.BorderRadius.circular(radius), - child: material.SizedBox( - height: 520, - child: material.Column( + child: material.ClipRRect( + borderRadius: material.BorderRadius.circular(radius), + child: material.SizedBox( + height: dialogH, + child: material.Column( mainAxisSize: material.MainAxisSize.min, crossAxisAlignment: material.CrossAxisAlignment.stretch, children: [ material.Padding( - padding: const material.EdgeInsets.fromLTRB(24, 24, 24, 8), + padding: material.EdgeInsets.fromLTRB(headerPadH, 20, headerPadH, 8), child: Column( crossAxisAlignment: material.CrossAxisAlignment.stretch, children: [ @@ -147,7 +159,7 @@ class _NewConnectionDialogContentState extends material.State<_NewConnectionDial crossAxisAlignment: material.CrossAxisAlignment.stretch, children: [ material.Container( - width: 140, + width: sidebarW, decoration: material.BoxDecoration( border: material.Border( right: material.BorderSide( @@ -186,16 +198,25 @@ class _NewConnectionDialogContentState extends material.State<_NewConnectionDial child: material.LayoutBuilder( builder: (context, constraints) { const spacing = 12.0; - const crossAxisCount = 4; - final cardWidth = (constraints.maxWidth - spacing * (crossAxisCount - 1)) / crossAxisCount; - const cardHeight = 132.0; + final gridPad = dialogMaxW < 420 ? 12.0 : 16.0; + final innerW = math.max(0.0, constraints.maxWidth - gridPad * 2); + final crossAxisCount = + WindowLayout.dbTypeGridCrossAxisCount(innerW); + final cardHeight = + WindowLayout.dbTypeCardHeight(crossAxisCount); + final cardWidth = crossAxisCount > 0 + ? (innerW - spacing * (crossAxisCount - 1)) / + crossAxisCount + : innerW; + final aspect = + cardHeight > 0 ? cardWidth / cardHeight : 1.0; return material.Padding( - padding: const material.EdgeInsets.all(16), + padding: material.EdgeInsets.all(gridPad), child: material.GridView.count( crossAxisCount: crossAxisCount, mainAxisSpacing: spacing, crossAxisSpacing: spacing, - childAspectRatio: cardWidth / cardHeight, + childAspectRatio: aspect.clamp(0.4, 4.0), shrinkWrap: true, physics: const material.ClampingScrollPhysics(), children: [ @@ -216,7 +237,10 @@ class _NewConnectionDialogContentState extends material.State<_NewConnectionDial ), ), material.Container( - padding: const material.EdgeInsets.symmetric(horizontal: 24, vertical: 16), + padding: material.EdgeInsets.symmetric( + horizontal: headerPadH, + vertical: 14, + ), decoration: material.BoxDecoration( border: material.Border( top: material.BorderSide(color: theme.border.withValues(alpha: 0.3)), @@ -241,7 +265,6 @@ class _NewConnectionDialogContentState extends material.State<_NewConnectionDial ), ], ), - ), ), ), ); @@ -317,7 +340,7 @@ class _DbTypeCardState extends material.State<_DbTypeCard> { child: material.AnimatedContainer( duration: const Duration(milliseconds: 120), curve: material.Curves.easeOut, - padding: const material.EdgeInsets.symmetric(vertical: 12, horizontal: 12), + padding: const material.EdgeInsets.symmetric(vertical: 10, horizontal: 8), decoration: material.BoxDecoration( color: highlighted ? t.muted.withValues(alpha: 0.4) : t.muted.withValues(alpha: 0.12), borderRadius: material.BorderRadius.circular(10), @@ -327,22 +350,52 @@ class _DbTypeCardState extends material.State<_DbTypeCard> { ), ), child: material.Column( - mainAxisSize: material.MainAxisSize.min, - mainAxisAlignment: material.MainAxisAlignment.center, + crossAxisAlignment: material.CrossAxisAlignment.stretch, children: [ - material.SizedBox( - width: 56, - height: 56, - child: widget.type.iconAsset != null - ? material.Image.asset( - widget.type.iconAsset!, - fit: material.BoxFit.contain, - filterQuality: material.FilterQuality.medium, - ) - : material.Icon(widget.type.icon, size: 56, color: t.primary), + material.Expanded( + child: material.Center( + child: material.SizedBox( + width: 52, + height: 52, + child: widget.type.iconAsset != null + ? material.Image.asset( + widget.type.iconAsset!, + fit: material.BoxFit.contain, + filterQuality: material.FilterQuality.medium, + ) + : material.Icon(widget.type.icon, size: 52, color: t.primary), + ), + ), + ), + const material.SizedBox(height: 6), + material.LayoutBuilder( + builder: (context, lc) { + return material.SizedBox( + height: 38, + child: material.FittedBox( + fit: material.BoxFit.scaleDown, + alignment: material.Alignment.center, + child: material.ConstrainedBox( + constraints: material.BoxConstraints( + maxWidth: math.max(48.0, lc.maxWidth), + ), + child: material.Text( + widget.type.label, + textAlign: material.TextAlign.center, + maxLines: 2, + overflow: material.TextOverflow.ellipsis, + style: material.TextStyle( + fontSize: 13, + fontWeight: material.FontWeight.w600, + height: 1.2, + color: t.foreground, + ), + ), + ), + ), + ); + }, ), - const material.SizedBox(height: 10), - Text(widget.type.label).semiBold().small(), ], ), ), diff --git a/lib/features/connections/new_folder_dialog.dart b/lib/features/connections/new_folder_dialog.dart index 8dae9849..15c95a5d 100644 --- a/lib/features/connections/new_folder_dialog.dart +++ b/lib/features/connections/new_folder_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart' as material; +import 'package:querya_desktop/core/layout/window_layout.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; /// Shows a dialog to create a new folder (same style as new connection). @@ -6,10 +7,10 @@ import 'package:querya_desktop/shared/widgets/widgets.dart'; Future showNewFolderDialog(BuildContext context) { return showAppDialog( context: context, - builder: (context) => const material.Dialog( + builder: (context) => material.Dialog( backgroundColor: material.Colors.transparent, - insetPadding: material.EdgeInsets.symmetric(horizontal: 40, vertical: 24), - child: _NewFolderDialogContent(), + insetPadding: WindowLayout.dialogSymmetricInsets(context), + child: const _NewFolderDialogContent(), ), ); } diff --git a/lib/features/main_screen/main_screen.dart b/lib/features/main_screen/main_screen.dart index e9882cf0..0b424b2f 100644 --- a/lib/features/main_screen/main_screen.dart +++ b/lib/features/main_screen/main_screen.dart @@ -4,9 +4,10 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart' as material show Scaffold, Container, MainAxisSize, GestureDetector, MouseRegion, SystemMouseCursors, HitTestBehavior, Icons, Icon; import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/core/theme/app_theme.dart'; -import 'package:querya_desktop/shared/widgets/widgets.dart'; - +import 'package:querya_desktop/core/theme/querya_colors.dart'; +import 'package:querya_desktop/features/connections/connection_creation_flow.dart'; import 'package:querya_desktop/features/connections/connections_panel.dart'; +import 'package:querya_desktop/shared/widgets/widgets.dart'; import 'package:querya_desktop/features/mysql/mysql_object_kind.dart'; import 'package:querya_desktop/features/postgresql/postgres_object_kind.dart'; import 'package:querya_desktop/features/connections/driver_manager_dialog.dart'; @@ -21,6 +22,9 @@ class MainScreen extends StatefulWidget { } class _MainScreenState extends State { + final GlobalKey _connectionsPanelKey = + GlobalKey(); + static const double _minLeftWidth = 180; static const double _maxLeftWidth = 500; /// Minimum width reserved for workspace (avoid Row overflow when window is narrow). @@ -142,6 +146,13 @@ class _MainScreenState extends State { }); } + Future _openNewConnectionFromHero() async { + final row = await promptCreateConnection(context); + if (!mounted || row == null) return; + await LocalDb.instance.addConnection(row); + await _connectionsPanelKey.currentState?.reloadConnectionsFromDb(); + } + @override Widget build(BuildContext context) { final theme = AppTheme.dark.colorScheme; @@ -150,12 +161,12 @@ class _MainScreenState extends State { body: Theme( data: AppTheme.dark, child: WindowBorder( - color: theme.border.withValues(alpha: 0.6), + color: theme.border.withValues(alpha: 0.35), width: 1, child: Column( children: [ _CustomTitleBar(theme: theme), - const Divider(height: 1), + Divider(height: 1, color: theme.border.withValues(alpha: 0.22)), Expanded( child: LayoutBuilder( builder: (context, constraints) { @@ -178,6 +189,8 @@ class _MainScreenState extends State { SizedBox( width: leftW, child: ConnectionsPanel( + key: _connectionsPanelKey, + selectedConnectionId: _activeConnection?.id, onConnectionSelected: _onConnectionSelected, onRedisDatabaseSelected: _onRedisDatabaseSelected, onMongoDBDatabaseSelected: _onMongoDBDatabaseSelected, @@ -216,6 +229,9 @@ class _MainScreenState extends State { postgresSqlTabRequestToken: _postgresSqlTabRequestToken, selectedMysqlObject: _selectedMysqlObject, mysqlSqlTabRequestToken: _mysqlSqlTabRequestToken, + onRequestNewConnection: () { + _openNewConnectionFromHero(); + }, ), ), ], @@ -292,6 +308,12 @@ class _CustomTitleBarState extends State<_CustomTitleBar> { child: Row( children: [ const SizedBox(width: 16), + const material.Icon( + material.Icons.search_rounded, + size: 18, + color: QueryaColors.accentCyan, + ), + const Gap(8), const Text('Querya').semiBold().small(), const Gap(24), Menubar( diff --git a/lib/features/main_screen/query_editor_tab.dart b/lib/features/main_screen/query_editor_tab.dart index 240e47b6..7272997e 100644 --- a/lib/features/main_screen/query_editor_tab.dart +++ b/lib/features/main_screen/query_editor_tab.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart' as material - show Padding, EdgeInsets, TextStyle, TextEditingController; + show EdgeInsets, Padding, TextEditingController, TextStyle; +import 'package:querya_desktop/core/theme/querya_typography.dart'; +import 'package:querya_desktop/features/main_screen/sql_editor_chrome.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; class QueryEditorTab extends StatelessWidget { @@ -68,17 +70,18 @@ class _QueryEditorBodyState extends State<_QueryEditorBody> { @override Widget build(BuildContext context) { + final theme = Theme.of(context); return material.Padding( padding: const material.EdgeInsets.all(12), - child: Card( - padding: material.EdgeInsets.zero, + child: SqlEditorChrome( child: TextField( controller: _owned, maxLines: null, expands: true, - style: const material.TextStyle( - fontFamily: 'monospace', + style: material.TextStyle( + fontFamily: QueryaTypography.mono, fontSize: 13, + color: theme.colorScheme.foreground, ), placeholder: const Text('-- Enter SQL here…\nSELECT 1;'), ), diff --git a/lib/features/main_screen/sql_editor_chrome.dart b/lib/features/main_screen/sql_editor_chrome.dart new file mode 100644 index 00000000..88014e14 --- /dev/null +++ b/lib/features/main_screen/sql_editor_chrome.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart' as material; +import 'package:querya_desktop/core/theme/querya_colors.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +/// Outer chrome for SQL editors: subtle border, surface fill, soft cyan glow. +class SqlEditorChrome extends StatelessWidget { + const SqlEditorChrome({super.key, required this.child}); + + final Widget child; + + static material.BoxDecoration inlineFieldDecoration(ThemeData theme) { + final cs = theme.colorScheme; + return material.BoxDecoration( + color: cs.card, + borderRadius: material.BorderRadius.circular(10), + border: material.Border.all( + color: cs.border.withValues(alpha: 0.45), + ), + boxShadow: [ + material.BoxShadow( + color: QueryaColors.accentCyan.withValues(alpha: 0.07), + blurRadius: 18, + offset: const material.Offset(0, 6), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final cs = theme.colorScheme; + final glow = QueryaColors.accentCyan.withValues(alpha: 0.1); + return material.Container( + decoration: material.BoxDecoration( + borderRadius: material.BorderRadius.circular(14), + boxShadow: [ + material.BoxShadow( + color: glow, + blurRadius: 28, + spreadRadius: 0, + offset: const material.Offset(0, 10), + ), + ], + ), + child: material.Container( + decoration: material.BoxDecoration( + color: cs.card, + borderRadius: material.BorderRadius.circular(14), + border: material.Border.all( + color: cs.border.withValues(alpha: 0.5), + ), + ), + clipBehavior: material.Clip.antiAlias, + child: child, + ), + ); + } +} diff --git a/lib/features/main_screen/workspace_empty_hero.dart b/lib/features/main_screen/workspace_empty_hero.dart new file mode 100644 index 00000000..7c0828f6 --- /dev/null +++ b/lib/features/main_screen/workspace_empty_hero.dart @@ -0,0 +1,322 @@ +import 'package:flutter/material.dart' as material; +import 'package:querya_desktop/core/layout/window_layout.dart'; +import 'package:querya_desktop/core/theme/querya_colors.dart'; +import 'package:querya_desktop/core/theme/querya_typography.dart'; +import 'package:querya_desktop/shared/widgets/widgets.dart'; + +/// Marketing-style empty workspace: badge, copy, mock window, primary CTA. +class WorkspaceEmptyHero extends StatelessWidget { + const WorkspaceEmptyHero({ + super.key, + required this.onNewConnection, + }); + + final VoidCallback onNewConnection; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final cs = theme.colorScheme; + return material.LayoutBuilder( + builder: (context, c) { + final vw = c.maxWidth; + final padH = WindowLayout.heroHorizontalPadding(vw); + final padV = vw < WindowLayout.compactWindowWidth ? 24.0 : 32.0; + final maxContent = WindowLayout.heroContentMaxWidth(vw); + final mockH = WindowLayout.heroMockWindowHeight(maxContent); + final compact = vw < WindowLayout.compactWindowWidth; + return material.SingleChildScrollView( + padding: material.EdgeInsets.symmetric(horizontal: padH, vertical: padV), + child: material.Align( + alignment: material.Alignment.topCenter, + child: material.ConstrainedBox( + constraints: material.BoxConstraints(maxWidth: maxContent), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.center, + children: [ + _HeroBadge(colorScheme: cs, compact: compact), + material.SizedBox(height: compact ? 16 : 20), + material.Padding( + padding: const material.EdgeInsets.symmetric(horizontal: 4), + child: material.Text( + 'A lightweight desktop client for every database you ship', + textAlign: material.TextAlign.center, + style: material.TextStyle( + fontSize: 20, + fontWeight: material.FontWeight.w600, + color: cs.foreground, + ), + ), + ), + material.SizedBox(height: compact ? 10 : 12), + material.Padding( + padding: const material.EdgeInsets.symmetric(horizontal: 4), + child: material.Text( + 'Connect to PostgreSQL, MySQL, Redis and MongoDB from a single ' + 'focused app with a calm dark interface.', + textAlign: material.TextAlign.center, + style: material.TextStyle( + fontSize: 14, + height: 1.4, + color: cs.mutedForeground, + ), + ), + ), + const material.SizedBox(height: 10), + Text( + 'BUILT WITH FLUTTER · CROSS-PLATFORM', + textAlign: material.TextAlign.center, + style: material.TextStyle( + fontFamily: QueryaTypography.mono, + fontSize: compact ? 9 : 10, + letterSpacing: 0.6, + color: cs.mutedForeground.withValues(alpha: 0.85), + ), + ), + material.SizedBox(height: compact ? 20 : 28), + _MockAppWindow( + colorScheme: cs, + height: mockH, + compact: compact, + ), + material.SizedBox(height: compact ? 20 : 28), + material.Wrap( + alignment: material.WrapAlignment.center, + spacing: 12, + runSpacing: 12, + children: [ + PrimaryButton( + onPressed: onNewConnection, + leading: material.Icon( + material.Icons.add_link_rounded, + size: compact ? 16 : 18, + ), + child: const Text('New connection'), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); + } +} + +class _HeroBadge extends StatelessWidget { + const _HeroBadge({required this.colorScheme, this.compact = false}); + + final ColorScheme colorScheme; + final bool compact; + + @override + Widget build(BuildContext context) { + return material.Container( + padding: material.EdgeInsets.symmetric( + horizontal: compact ? 10 : 12, + vertical: compact ? 5 : 6, + ), + decoration: material.BoxDecoration( + color: colorScheme.background, + borderRadius: material.BorderRadius.circular(999), + border: material.Border.all( + color: QueryaColors.accentCyan.withValues(alpha: 0.45), + ), + ), + child: material.Row( + mainAxisSize: material.MainAxisSize.max, + children: [ + material.Icon( + material.Icons.auto_awesome_rounded, + size: compact ? 12 : 14, + color: colorScheme.primary, + ), + material.SizedBox(width: compact ? 6 : 8), + material.Expanded( + child: material.Text( + 'One app for SQL and NoSQL', + maxLines: 2, + overflow: material.TextOverflow.ellipsis, + style: material.TextStyle( + fontSize: compact ? 11 : 12, + color: colorScheme.foreground.withValues(alpha: 0.92), + ), + ), + ), + ], + ), + ); + } +} + +class _MockAppWindow extends StatelessWidget { + const _MockAppWindow({ + required this.colorScheme, + required this.height, + this.compact = false, + }); + + final ColorScheme colorScheme; + final double height; + final bool compact; + + @override + Widget build(BuildContext context) { + final glow = QueryaColors.accentCyan.withValues(alpha: 0.14); + final sidebarW = compact ? 58.0 : 72.0; + final blur = compact ? 28.0 : 40.0; + final radius = compact ? 12.0 : 16.0; + return material.Container( + decoration: material.BoxDecoration( + borderRadius: material.BorderRadius.circular(radius), + boxShadow: [ + material.BoxShadow( + color: glow, + blurRadius: blur, + spreadRadius: -4, + offset: material.Offset(0, compact ? 12 : 18), + ), + ], + ), + child: material.Container( + height: height, + decoration: material.BoxDecoration( + color: const Color(0xFF141414), + borderRadius: material.BorderRadius.circular(radius), + border: material.Border.all( + color: colorScheme.border.withValues(alpha: 0.5), + ), + ), + clipBehavior: material.Clip.antiAlias, + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Container( + padding: material.EdgeInsets.symmetric( + horizontal: compact ? 10 : 12, + vertical: compact ? 8 : 10, + ), + decoration: material.BoxDecoration( + border: material.Border( + bottom: material.BorderSide( + color: colorScheme.border.withValues(alpha: 0.35), + ), + ), + ), + child: material.Row( + children: [ + _trafficDot(const Color(0xFFFF5F57)), + const material.SizedBox(width: 6), + _trafficDot(const Color(0xFFFEBC2E)), + const material.SizedBox(width: 6), + _trafficDot(const Color(0xFF28C840)), + ], + ), + ), + material.Expanded( + child: material.Row( + crossAxisAlignment: material.CrossAxisAlignment.stretch, + children: [ + material.Container( + width: sidebarW, + padding: material.EdgeInsets.fromLTRB( + compact ? 8 : 10, + compact ? 8 : 10, + compact ? 6 : 8, + compact ? 8 : 10, + ), + color: const Color(0xFF0F0F0F), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + Text( + 'SERVERS', + style: material.TextStyle( + fontFamily: QueryaTypography.mono, + fontSize: compact ? 8 : 9, + letterSpacing: 0.5, + color: colorScheme.mutedForeground, + ), + ), + material.SizedBox(height: compact ? 8 : 10), + material.Container( + padding: material.EdgeInsets.symmetric( + horizontal: compact ? 6 : 8, + vertical: compact ? 4 : 6, + ), + decoration: material.BoxDecoration( + color: colorScheme.primary.withValues(alpha: 0.2), + borderRadius: + material.BorderRadius.circular(compact ? 6 : 8), + ), + child: material.Text( + 'analytics-prod', + maxLines: 1, + overflow: material.TextOverflow.ellipsis, + style: material.TextStyle( + fontSize: compact ? 9 : 10, + color: colorScheme.foreground, + ), + ), + ), + ], + ), + ), + material.Expanded( + child: material.Container( + margin: material.EdgeInsets.all(compact ? 8 : 10), + padding: material.EdgeInsets.all(compact ? 8 : 10), + decoration: material.BoxDecoration( + color: const Color(0xFF0A0A0A), + borderRadius: material.BorderRadius.circular(8), + border: material.Border.all( + color: colorScheme.border.withValues(alpha: 0.25), + ), + ), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + material.Text( + 'SELECT …', + style: material.TextStyle( + fontFamily: QueryaTypography.mono, + fontSize: compact ? 9 : 11, + height: 1.45, + color: colorScheme.primary, + ), + ), + material.Text( + " interval '1 day'", + style: material.TextStyle( + fontFamily: QueryaTypography.mono, + fontSize: compact ? 9 : 11, + height: 1.45, + color: const Color(0xFFFBBF24), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +material.Widget _trafficDot(Color c) { + return material.Container( + width: 10, + height: 10, + decoration: material.BoxDecoration( + color: c, + shape: material.BoxShape.circle, + ), + ); +} diff --git a/lib/features/main_screen/workspace_panel.dart b/lib/features/main_screen/workspace_panel.dart index 1cbad5c3..beee2950 100644 --- a/lib/features/main_screen/workspace_panel.dart +++ b/lib/features/main_screen/workspace_panel.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart' as material show Axis, Container, EdgeInsets, BoxDecoration, GestureDetector, Padding, BorderRadius, Center, CrossAxisAlignment, Icon, Icons, MouseRegion, AnimatedContainer, AnimatedScale, Curves, SystemMouseCursors, LayoutBuilder, HitTestBehavior, SizedBox, SingleChildScrollView, Row, MainAxisSize; +import 'package:flutter/material.dart' as material show Alignment, Axis, Container, EdgeInsets, BoxDecoration, GestureDetector, Padding, BorderRadius, Center, CrossAxisAlignment, Icon, Icons, MouseRegion, AnimatedContainer, AnimatedScale, Curves, SystemMouseCursors, LayoutBuilder, HitTestBehavior, SizedBox, SingleChildScrollView, Row, MainAxisSize; import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; @@ -17,6 +17,7 @@ import 'package:querya_desktop/features/redis/redis_explorer_view.dart'; import 'package:querya_desktop/features/redis/redis_view.dart'; import 'query_editor_tab.dart'; import 'results_tab.dart'; +import 'workspace_empty_hero.dart'; Widget _pgObjectWorkspace({ required ConnectionRow connection, @@ -122,6 +123,7 @@ class WorkspacePanel extends StatefulWidget { this.postgresSqlTabRequestToken = 0, this.selectedMysqlObject, this.mysqlSqlTabRequestToken = 0, + this.onRequestNewConnection, }); /// Currently selected connection from the sidebar. @@ -150,6 +152,9 @@ class WorkspacePanel extends StatefulWidget { /// Incremented by [MainScreen] to switch the MySQL home view to the SQL tab. final int mysqlSqlTabRequestToken; + /// Empty-state hero: primary CTA to add a connection. + final void Function()? onRequestNewConnection; + @override State createState() => _WorkspacePanelState(); } @@ -163,6 +168,17 @@ class _WorkspacePanelState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); + if (widget.activeConnection == null) { + return material.Container( + color: theme.colorScheme.background, + alignment: material.Alignment.topCenter, + child: WorkspaceEmptyHero( + onNewConnection: + widget.onRequestNewConnection ?? () {}, + ), + ); + } + // If a PostgreSQL connection is selected → stats, table/view, function, or sequence if (widget.activeConnection != null && widget.activeConnection!.type == 'postgresql') { diff --git a/lib/features/mongodb/mongo_database_dialog.dart b/lib/features/mongodb/mongo_database_dialog.dart index a833dad4..dcf15582 100644 --- a/lib/features/mongodb/mongo_database_dialog.dart +++ b/lib/features/mongodb/mongo_database_dialog.dart @@ -1,14 +1,15 @@ import 'package:flutter/material.dart' as material; +import 'package:querya_desktop/core/layout/window_layout.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; /// Shows dialog to create a new MongoDB database. Future showCreateMongoDBDialog(material.BuildContext context) async { return showAppDialog( context: context, - builder: (context) => const material.Dialog( + builder: (context) => material.Dialog( backgroundColor: material.Colors.transparent, - insetPadding: material.EdgeInsets.symmetric(horizontal: 40, vertical: 24), - child: _CreateMongoDBDialogContent(), + insetPadding: WindowLayout.dialogSymmetricInsets(context), + child: const _CreateMongoDBDialogContent(), ), ); } diff --git a/lib/features/mongodb/mongodb_connection_form.dart b/lib/features/mongodb/mongodb_connection_form.dart index e998ca3f..f0526ece 100644 --- a/lib/features/mongodb/mongodb_connection_form.dart +++ b/lib/features/mongodb/mongodb_connection_form.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart' as material; import 'package:querya_desktop/core/database/mongodb_connection.dart'; +import 'package:querya_desktop/core/layout/window_layout.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; @@ -47,7 +48,7 @@ Future showMongoConnectionForm( context: context, builder: (context) => material.Dialog( backgroundColor: material.Colors.transparent, - insetPadding: const material.EdgeInsets.symmetric(horizontal: 40, vertical: 24), + insetPadding: WindowLayout.dialogSymmetricInsets(context), child: _MongoConnectionFormContent(folderId: folderId), ), ); diff --git a/lib/features/mysql/mysql_connection_form.dart b/lib/features/mysql/mysql_connection_form.dart index 92ba4cee..8227115f 100644 --- a/lib/features/mysql/mysql_connection_form.dart +++ b/lib/features/mysql/mysql_connection_form.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart' as material; import 'package:querya_desktop/core/database/mysql_connection.dart'; +import 'package:querya_desktop/core/layout/window_layout.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; @@ -15,8 +16,7 @@ Future showMysqlConnectionForm( context: context, builder: (context) => material.Dialog( backgroundColor: material.Colors.transparent, - insetPadding: - const material.EdgeInsets.symmetric(horizontal: 40, vertical: 24), + insetPadding: WindowLayout.dialogSymmetricInsets(context), child: _MysqlConnectionFormContent(folderId: folderId), ), ); diff --git a/lib/features/mysql/mysql_sql_editor_dialog.dart b/lib/features/mysql/mysql_sql_editor_dialog.dart index b75d472a..eb69d33d 100644 --- a/lib/features/mysql/mysql_sql_editor_dialog.dart +++ b/lib/features/mysql/mysql_sql_editor_dialog.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart' as material; +import 'package:querya_desktop/core/layout/window_layout.dart'; +import 'package:querya_desktop/core/theme/querya_typography.dart'; +import 'package:querya_desktop/features/main_screen/sql_editor_chrome.dart'; import 'package:querya_desktop/features/mysql/mysql_table_utils.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; @@ -72,8 +75,7 @@ class _MysqlSqlEditorDialogState extends material.State<_MysqlSqlEditorDialog> { final radius = Theme.of(context).radiusXxl; return material.Dialog( backgroundColor: material.Colors.transparent, - insetPadding: - const material.EdgeInsets.symmetric(horizontal: 32, vertical: 24), + insetPadding: WindowLayout.dialogSymmetricInsets(context), child: material.Container( constraints: const material.BoxConstraints( maxWidth: 720, @@ -110,11 +112,8 @@ class _MysqlSqlEditorDialogState extends material.State<_MysqlSqlEditorDialog> { child: material.SizedBox( height: 280, child: material.Container( - decoration: material.BoxDecoration( - color: theme.muted.withValues(alpha: 0.15), - borderRadius: material.BorderRadius.circular(8), - border: material.Border.all( - color: theme.border.withValues(alpha: 0.4)), + decoration: SqlEditorChrome.inlineFieldDecoration( + Theme.of(context), ), child: material.TextField( controller: _controller, @@ -122,7 +121,7 @@ class _MysqlSqlEditorDialogState extends material.State<_MysqlSqlEditorDialog> { expands: true, textAlignVertical: material.TextAlignVertical.top, style: material.TextStyle( - fontFamily: 'monospace', + fontFamily: QueryaTypography.mono, fontSize: 12, color: theme.foreground, ), diff --git a/lib/features/postgresql/postgres_sql_editor_dialog.dart b/lib/features/postgresql/postgres_sql_editor_dialog.dart index e1bf4a9d..86dd0328 100644 --- a/lib/features/postgresql/postgres_sql_editor_dialog.dart +++ b/lib/features/postgresql/postgres_sql_editor_dialog.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart' as material; +import 'package:querya_desktop/core/layout/window_layout.dart'; +import 'package:querya_desktop/core/theme/querya_typography.dart'; +import 'package:querya_desktop/features/main_screen/sql_editor_chrome.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; /// Returns true if [sql] is allowed to run (read-only: SELECT / WITH). @@ -99,8 +102,7 @@ class _PostgresSqlEditorDialogState extends material.State<_PostgresSqlEditorDia final radius = Theme.of(context).radiusXxl; return material.Dialog( backgroundColor: material.Colors.transparent, - insetPadding: - const material.EdgeInsets.symmetric(horizontal: 32, vertical: 24), + insetPadding: WindowLayout.dialogSymmetricInsets(context), child: material.Container( constraints: const material.BoxConstraints( maxWidth: 720, @@ -137,11 +139,8 @@ class _PostgresSqlEditorDialogState extends material.State<_PostgresSqlEditorDia child: material.SizedBox( height: 280, child: material.Container( - decoration: material.BoxDecoration( - color: theme.muted.withValues(alpha: 0.15), - borderRadius: material.BorderRadius.circular(8), - border: material.Border.all( - color: theme.border.withValues(alpha: 0.4)), + decoration: SqlEditorChrome.inlineFieldDecoration( + Theme.of(context), ), child: material.TextField( controller: _controller, @@ -149,7 +148,7 @@ class _PostgresSqlEditorDialogState extends material.State<_PostgresSqlEditorDia expands: true, textAlignVertical: material.TextAlignVertical.top, style: material.TextStyle( - fontFamily: 'monospace', + fontFamily: QueryaTypography.mono, fontSize: 12, color: theme.foreground, ), diff --git a/lib/features/postgresql/postgres_table_privileges_dialog.dart b/lib/features/postgresql/postgres_table_privileges_dialog.dart index 3130f008..512055d1 100644 --- a/lib/features/postgresql/postgres_table_privileges_dialog.dart +++ b/lib/features/postgresql/postgres_table_privileges_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart' as material; import 'package:querya_desktop/core/database/postgres_connection.dart'; +import 'package:querya_desktop/core/layout/window_layout.dart'; import 'package:querya_desktop/core/database/postgres_metadata.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; @@ -79,8 +80,7 @@ class _PrivilegesDialogBodyState extends material.State<_PrivilegesDialogBody> { final radius = Theme.of(context).radiusXxl; return material.Dialog( backgroundColor: material.Colors.transparent, - insetPadding: - const material.EdgeInsets.symmetric(horizontal: 32, vertical: 24), + insetPadding: WindowLayout.dialogSymmetricInsets(context), child: material.Container( constraints: const material.BoxConstraints( maxWidth: 560, diff --git a/lib/features/postgresql/postgresql_connection_form.dart b/lib/features/postgresql/postgresql_connection_form.dart index 79a7b779..30ab6670 100644 --- a/lib/features/postgresql/postgresql_connection_form.dart +++ b/lib/features/postgresql/postgresql_connection_form.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart' as material; import 'package:querya_desktop/core/database/postgres_connection.dart'; +import 'package:querya_desktop/core/layout/window_layout.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; @@ -15,8 +16,7 @@ Future showPostgresConnectionForm( context: context, builder: (context) => material.Dialog( backgroundColor: material.Colors.transparent, - insetPadding: - const material.EdgeInsets.symmetric(horizontal: 40, vertical: 24), + insetPadding: WindowLayout.dialogSymmetricInsets(context), child: _PostgresConnectionFormContent(folderId: folderId), ), ); diff --git a/lib/features/redis/redis_connection_form.dart b/lib/features/redis/redis_connection_form.dart index d79d3c74..798b3a2c 100644 --- a/lib/features/redis/redis_connection_form.dart +++ b/lib/features/redis/redis_connection_form.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart' as material; import 'package:querya_desktop/core/database/redis_connection.dart'; +import 'package:querya_desktop/core/layout/window_layout.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; @@ -14,7 +15,7 @@ Future showRedisConnectionForm( context: context, builder: (context) => material.Dialog( backgroundColor: material.Colors.transparent, - insetPadding: const material.EdgeInsets.symmetric(horizontal: 40, vertical: 24), + insetPadding: WindowLayout.dialogSymmetricInsets(context), child: _RedisConnectionFormContent(folderId: folderId), ), ); diff --git a/test/features/main_screen/workspace_panel_layout_test.dart b/test/features/main_screen/workspace_panel_layout_test.dart index b92fac09..d65f43e1 100644 --- a/test/features/main_screen/workspace_panel_layout_test.dart +++ b/test/features/main_screen/workspace_panel_layout_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart' as material; import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/core/theme/app_theme.dart'; import 'package:querya_desktop/features/main_screen/workspace_panel.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; @@ -7,6 +8,17 @@ import 'package:shadcn_flutter/shadcn_flutter.dart'; import '../../support/layout_overflow.dart'; void main() { + /// Type must not be postgresql/mysql/mongodb/redis so [WorkspacePanel] uses the + /// default Query/Output split (includes resize handle). Not a real DB driver. + const stubSplitWorkspaceConnection = ConnectionRow( + id: 1, + type: '_layout_test_split', + name: 'layout-stub', + host: '127.0.0.1', + port: 0, + createdAt: '0', + ); + group('WorkspacePanel layout (no connection)', () { final sizes = { 'narrow_tall': const material.Size(320, 720), @@ -46,7 +58,7 @@ void main() { darkTheme: AppTheme.dark, themeMode: ThemeMode.dark, home: const material.SizedBox.expand( - child: WorkspacePanel(), + child: WorkspacePanel(activeConnection: stubSplitWorkspaceConnection), ), ), );