From 93b709dc145f625f553927318c809346ef3e92f6 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Tue, 28 Jul 2026 13:20:01 +0300 Subject: [PATCH 1/3] ui(trees): align Redis sidebar tree with shared row design Use _PgTreeRow for db nodes, add Databases group header, TreeLoadError, and database leaf icons so Redis matches PG/MySQL tree chrome. --- .../connections/connections_panel_redis.dart | 146 +++++++++++------- 1 file changed, 92 insertions(+), 54 deletions(-) diff --git a/lib/features/connections/connections_panel_redis.dart b/lib/features/connections/connections_panel_redis.dart index c23254e..2e01abf 100644 --- a/lib/features/connections/connections_panel_redis.dart +++ b/lib/features/connections/connections_panel_redis.dart @@ -258,22 +258,24 @@ class _RedisConnectionTileState extends State<_RedisConnectionTile> { ), ), if (_error != null) - material.Padding( + TreeLoadError( + message: _error!, padding: const material.EdgeInsets.only( - left: 28, top: 4, bottom: 4), - child: material.Text( - 'Error', - overflow: material.TextOverflow.ellipsis, - maxLines: 1, - style: material.TextStyle( - fontSize: 11, color: theme.colorScheme.destructive), + left: 28, + top: 4, + bottom: 4, ), + onRetry: _loadDatabases, ), - for (final db in _databases) - _RedisDatabaseNode( - index: db.index, - keys: db.keys, - onTap: () => widget.onDatabaseTap?.call(db.index), + if (_databases.isNotEmpty) + _RedisDatabasesNode( + connection: widget.connection, + databases: _databases, + onRefreshDatabases: () { + setState(() => _databases = []); + _loadDatabases(); + }, + onDatabaseTap: widget.onDatabaseTap, ), ], ), @@ -285,6 +287,60 @@ class _RedisConnectionTileState extends State<_RedisConnectionTile> { } } +class _RedisDatabasesNode extends material.StatelessWidget { + const _RedisDatabasesNode({ + required this.connection, + required this.databases, + required this.onRefreshDatabases, + this.onDatabaseTap, + }); + + final ConnectionRow connection; + final List<({int index, int keys})> databases; + final VoidCallback onRefreshDatabases; + final void Function(int database)? onDatabaseTap; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return material.Padding( + padding: const material.EdgeInsets.only(left: 20), + child: material.Column( + crossAxisAlignment: material.CrossAxisAlignment.start, + mainAxisSize: material.MainAxisSize.min, + children: [ + _PgTreeRow( + label: 'Databases (${databases.length})', + icon: QueryaIcons.databasesFolder, + iconSize: QueryaIconSizes.treeConnection, + iconColor: theme.colorScheme.primary.withValues(alpha: 0.7), + textStyle: material.TextStyle( + fontSize: 12, + color: theme.colorScheme.foreground, + ), + verticalPadding: 4, + onTap: null, + connection: connection, + onContextRefresh: onRefreshDatabases, + ), + lazyConnectionTreeList( + context: context, + itemCount: databases.length, + itemBuilder: (context, index) { + final db = databases[index]; + return _RedisDatabaseNode( + index: db.index, + keys: db.keys, + onTap: () => onDatabaseTap?.call(db.index), + ); + }, + ), + ], + ), + ); + } +} + class _RedisDatabaseNode extends StatelessWidget { const _RedisDatabaseNode({ required this.index, @@ -300,49 +356,31 @@ class _RedisDatabaseNode extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); return material.Padding( - padding: const material.EdgeInsets.only(left: 24), - 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: 5), - child: material.Row( - children: [ - material.Icon( - QueryaIcons.databasesFolder, - size: 14, - color: keys > 0 - ? theme.colorScheme.primary.withValues(alpha: 0.7) - : theme.colorScheme.mutedForeground - .withValues(alpha: 0.5), + padding: const material.EdgeInsets.only(left: 16), + child: _PgTreeRow( + label: 'db$index', + icon: QueryaIcons.database, + iconSize: QueryaIconSizes.treeConnection, + iconColor: keys > 0 + ? theme.colorScheme.primary.withValues(alpha: 0.7) + : theme.colorScheme.mutedForeground.withValues(alpha: 0.5), + trailing: keys > 0 + ? material.Text( + '$keys', + style: material.TextStyle( + fontSize: 10, + color: theme.colorScheme.mutedForeground, ), - const Gap(8), - material.Expanded( - child: material.Text( - 'db$index', - overflow: material.TextOverflow.ellipsis, - maxLines: 1, - style: material.TextStyle( - fontSize: 12, - color: keys > 0 - ? theme.colorScheme.foreground - : theme.colorScheme.mutedForeground, - ), - ), - ), - if (keys > 0) - material.Text( - '$keys', - style: material.TextStyle( - fontSize: 10, color: theme.colorScheme.mutedForeground), - ), - ], - ), - ), + ) + : null, + textStyle: material.TextStyle( + fontSize: 12, + color: keys > 0 + ? theme.colorScheme.foreground + : theme.colorScheme.mutedForeground, ), + verticalPadding: 3, + onTap: onTap, ), ); } From 906bcd6803375be86b56a0c0a7585ed033aed6ad Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Tue, 28 Jul 2026 13:20:04 +0300 Subject: [PATCH 2/3] ui(trees): unify SDUI/SQLite leaf styling and tree indentation Match extension and SDUI trees to native row tokens (chevron, sizes, group vs leaf icons), and shift terminal entities right for clearer hierarchy across PG, MySQL, SQLite, and extension drivers. --- lib/core/sdui/sdui_tree_builder.dart | 34 ++++++++++++++----- lib/core/ui/querya_icons.dart | 4 +-- .../connections_panel_extension.dart | 2 +- .../connections/connections_panel_mysql.dart | 2 +- .../connections_panel_pg_tree.dart | 2 +- .../connections/connections_panel_sqlite.dart | 4 +-- 6 files changed, 32 insertions(+), 16 deletions(-) diff --git a/lib/core/sdui/sdui_tree_builder.dart b/lib/core/sdui/sdui_tree_builder.dart index 69d06c0..7ab1795 100644 --- a/lib/core/sdui/sdui_tree_builder.dart +++ b/lib/core/sdui/sdui_tree_builder.dart @@ -50,7 +50,7 @@ class SduiTreeBuilderState extends material.State { final Set _expanded = {}; final Map _expandErrors = {}; - static const double _rowExtent = 36; + static const double _rowExtent = 28; @override void initState() { @@ -180,17 +180,27 @@ class SduiTreeBuilderState extends material.State { } material.Widget _buildNodeRow(SduiTreeNode node, {required int depth}) { + final theme = material.Theme.of(context); + final muted = theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.85); + final primary = theme.colorScheme.primary; final canExpand = node.expandable || node.hasChildren; final isExpanded = _expanded.contains(node.id); final isLoading = _loading.contains(node.id); final nodeKind = _resolveNodeKind(node); final isBrowsable = nodeKind == 'table' || nodeKind == 'view'; + final iconSize = canExpand + ? QueryaIconSizes.treeGroup + : QueryaIconSizes.treeLeaf; + final iconColor = isBrowsable + ? primary.withValues(alpha: 0.5) + : muted; + final rowLeft = 8.0 + depth * 16.0 + (canExpand ? 0 : 4.0); return material.InkWell( onTap: isBrowsable ? () => widget.onNodeSelected?.call(node) : null, child: material.Padding( padding: material.EdgeInsets.only( - left: 8.0 + depth * 16.0, + left: rowLeft, right: 8, ), child: material.Row( @@ -201,7 +211,7 @@ class SduiTreeBuilderState extends material.State { height: 28, child: material.IconButton( padding: material.EdgeInsets.zero, - iconSize: 18, + iconSize: QueryaIconSizes.treeExpand, onPressed: () { if (isExpanded) { _onCollapse(node); @@ -209,10 +219,14 @@ class SduiTreeBuilderState extends material.State { _onExpand(node); } }, - icon: material.Icon( - isExpanded - ? material.Icons.expand_more - : material.Icons.chevron_right, + icon: material.AnimatedRotation( + turns: isExpanded ? 0.25 : 0, + duration: const Duration(milliseconds: 160), + curve: material.Curves.easeOutCubic, + child: const material.Icon( + QueryaIcons.expandClosed, + size: QueryaIconSizes.treeExpand, + ), ), ), ) @@ -230,7 +244,8 @@ class SduiTreeBuilderState extends material.State { node.icon, expandable: node.expandable, ), - size: QueryaIconSizes.sduiNode, + size: iconSize, + color: iconColor, ), const Gap(8), material.Expanded( @@ -239,7 +254,8 @@ class SduiTreeBuilderState extends material.State { overflow: material.TextOverflow.ellipsis, maxLines: 1, style: material.TextStyle( - fontSize: 12, + fontSize: 11, + color: isBrowsable ? theme.colorScheme.onSurface : muted, fontWeight: isBrowsable ? material.FontWeight.w600 : null, ), ), diff --git a/lib/core/ui/querya_icons.dart b/lib/core/ui/querya_icons.dart index f1d4daf..297dcf4 100644 --- a/lib/core/ui/querya_icons.dart +++ b/lib/core/ui/querya_icons.dart @@ -65,10 +65,10 @@ abstract final class QueryaIcons { case 'database': return database; case 'table': - return tableGroup; + return expandable ? tableGroup : tableLeaf; case 'view': case 'eye': - return viewGroup; + return expandable ? viewGroup : viewLeaf; case 'folder': case 'folder-table': return folder; diff --git a/lib/features/connections/connections_panel_extension.dart b/lib/features/connections/connections_panel_extension.dart index 19036f2..2874342 100644 --- a/lib/features/connections/connections_panel_extension.dart +++ b/lib/features/connections/connections_panel_extension.dart @@ -184,7 +184,7 @@ class _ExtensionConnectionTileState extends State<_ExtensionConnectionTile> { duration: context.motionDuration(QueryaMotion.fast), curve: context.motionCurve(QueryaMotion.standardCurve), child: material.Icon( - material.Icons.chevron_right_rounded, + QueryaIcons.expandClosed, size: 16, color: theme.colorScheme.mutedForeground, ), diff --git a/lib/features/connections/connections_panel_mysql.dart b/lib/features/connections/connections_panel_mysql.dart index 98fe6e5..eaf0e58 100644 --- a/lib/features/connections/connections_panel_mysql.dart +++ b/lib/features/connections/connections_panel_mysql.dart @@ -657,7 +657,7 @@ class _MysqlObjectGroupState extends State<_MysqlObjectGroup> { context: context, itemCount: widget.items.length, itemExtent: kConnectionTreeRowExtent, - padding: const material.EdgeInsets.only(left: 22), + padding: const material.EdgeInsets.only(left: 26), itemBuilder: (context, index) { final item = widget.items[index]; return _PgTreeRow( diff --git a/lib/features/connections/connections_panel_pg_tree.dart b/lib/features/connections/connections_panel_pg_tree.dart index a24d20d..4281af7 100644 --- a/lib/features/connections/connections_panel_pg_tree.dart +++ b/lib/features/connections/connections_panel_pg_tree.dart @@ -1035,7 +1035,7 @@ class _PgObjectGroupState extends State<_PgObjectGroup> { context: context, itemCount: widget.items.length, itemExtent: kConnectionTreeRowExtent, - padding: const material.EdgeInsets.only(left: 22), + padding: const material.EdgeInsets.only(left: 26), itemBuilder: (context, index) { final item = widget.items[index]; return _PgTreeRow( diff --git a/lib/features/connections/connections_panel_sqlite.dart b/lib/features/connections/connections_panel_sqlite.dart index 2efdee5..1456068 100644 --- a/lib/features/connections/connections_panel_sqlite.dart +++ b/lib/features/connections/connections_panel_sqlite.dart @@ -366,7 +366,7 @@ class _SqliteObjectGroupState extends State<_SqliteObjectGroup> { curve: context.motionCurve(QueryaMotion.standardCurve), child: material.Icon( QueryaIcons.expandClosed, - size: 13, + size: QueryaIconSizes.treeExpand, color: theme.colorScheme.mutedForeground, ), ), @@ -387,7 +387,7 @@ class _SqliteObjectGroupState extends State<_SqliteObjectGroup> { context: context, itemCount: widget.items.length, itemExtent: kConnectionTreeRowExtent, - padding: const material.EdgeInsets.only(left: 22), + padding: const material.EdgeInsets.only(left: 26), itemBuilder: (context, index) { final item = widget.items[index]; return _PgTreeRow( From 0036c7637a4f6a3325b777f4d45c4dd9d4581891 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Tue, 28 Jul 2026 13:25:02 +0300 Subject: [PATCH 3/3] test: update SDUI icon and tree builder expectations Reflect group vs leaf table/view icons and QueryaIcons.expandClosed chevron used by SduiTreeBuilder after tree visual unification. --- test/core/sdui/sdui_builders_test.dart | 3 ++- test/core/ui/querya_icons_test.dart | 14 +++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/test/core/sdui/sdui_builders_test.dart b/test/core/sdui/sdui_builders_test.dart index 3181f68..dd43253 100644 --- a/test/core/sdui/sdui_builders_test.dart +++ b/test/core/sdui/sdui_builders_test.dart @@ -4,6 +4,7 @@ import 'package:querya_desktop/core/sdui/sdui_form_builder.dart'; import 'package:querya_desktop/core/sdui/sdui_form_schema.dart'; import 'package:querya_desktop/core/sdui/sdui_tree_builder.dart'; import 'package:querya_desktop/core/sdui/sdui_tree_schema.dart'; +import 'package:querya_desktop/core/ui/querya_icons.dart'; import '../../support/querya_theme_test_shell.dart'; @@ -206,7 +207,7 @@ void main() { expect(find.text('Databases'), findsOneWidget); expect(find.text('analytics'), findsNothing); - await tester.tap(find.byIcon(material.Icons.chevron_right)); + await tester.tap(find.byIcon(QueryaIcons.expandClosed)); await tester.pumpAndSettle(); expect(fetches, 1); diff --git a/test/core/ui/querya_icons_test.dart b/test/core/ui/querya_icons_test.dart index bdeb3b5..7f93df8 100644 --- a/test/core/ui/querya_icons_test.dart +++ b/test/core/ui/querya_icons_test.dart @@ -53,9 +53,21 @@ void main() { QueryaIcons.database, ); expect( - QueryaIcons.sduiNodeIcon('table', expandable: false), + QueryaIcons.sduiNodeIcon('table', expandable: true), QueryaIcons.tableGroup, ); + expect( + QueryaIcons.sduiNodeIcon('table', expandable: false), + QueryaIcons.tableLeaf, + ); + expect( + QueryaIcons.sduiNodeIcon('view', expandable: true), + QueryaIcons.viewGroup, + ); + expect( + QueryaIcons.sduiNodeIcon('view', expandable: false), + QueryaIcons.viewLeaf, + ); expect( QueryaIcons.sduiNodeIcon(null, expandable: true), QueryaIcons.folder,