From fa157c9fa0cd7e346b7ec11fafb48c46e8c2d599 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 11 Oct 2026 15:53:11 +0300 Subject: [PATCH] fix(ui): rules use the workbench border colour through QueryaDivider Material's Divider reads the Material dividerColor and is near-black in the light theme (connection forms). QueryaDivider / QueryaDivider.vertical draw workbench.borderSubtle; the 85 Divider / VerticalDivider calls in lib use it and a guard test rejects the raw ones. Refs #1443 --- docs/ui-kit.md | 1 + .../command_palette_dialog.dart | 2 +- .../quick_switcher_dialog.dart | 2 +- .../connections/connections_panel.dart | 2 +- .../connections/driver_manager_dialog.dart | 2 +- .../connections/sqlite_connection_form.dart | 2 +- .../extensions/extension_workspace_home.dart | 2 +- .../pages/extension_manager_dialog.dart | 2 +- lib/features/main_screen/main_screen.dart | 2 +- .../mongodb/mongo_add_document_dialog.dart | 6 +- .../mongodb/mongo_aggregation_workspace.dart | 10 +-- .../mongodb/mongo_collections_view.dart | 2 +- .../mongodb/mongo_create_index_dialog.dart | 2 +- .../mongodb/mongo_database_dialog.dart | 4 +- .../mongodb/mongo_databases_view.dart | 2 +- .../mongodb/mongo_document_tree_view.dart | 3 +- .../mongodb/mongo_documents_view.dart | 4 +- lib/features/mongodb/mongo_explorer_view.dart | 2 +- lib/features/mongodb/mongo_indexes_view.dart | 2 +- .../mongodb/mongo_query_workspace.dart | 10 +-- lib/features/mongodb/mongo_stats_view.dart | 2 +- .../mongodb/mongodb_connection_form.dart | 4 +- lib/features/mysql/mysql_connection_form.dart | 4 +- lib/features/mysql/mysql_stats_view.dart | 2 +- lib/features/mysql/mysql_workspace_home.dart | 2 +- .../onboarding/welcome_tour_dialog.dart | 10 +-- .../postgresql/postgres_stats_view.dart | 2 +- .../postgres_table_privileges_dialog.dart | 4 +- .../postgresql/postgres_workspace_home.dart | 2 +- .../postgresql_connection_form.dart | 4 +- lib/features/redis/redis_connection_form.dart | 4 +- lib/features/redis/redis_explorer_view.dart | 2 +- lib/features/redis/redis_key_editor.dart | 2 +- lib/features/redis/redis_keys_view.dart | 2 +- lib/features/redis/redis_view.dart | 2 +- .../preferences_shortcuts_section.dart | 2 +- .../settings/theme_editor_dialog.dart | 2 +- .../sqlite/sqlite_workspace_home.dart | 2 +- .../workspace/data_grid_groupings_view.dart | 6 +- .../workspace/generic_sql_workspace.dart | 2 +- .../workspace/sql_query_history_dialog.dart | 2 +- lib/shared/widgets/querya_divider.dart | 73 +++++++++++++++++++ lib/shared/widgets/widgets.dart | 3 +- test/guards/no_raw_dividers_test.dart | 29 ++++++++ test/shared/widgets/querya_divider_test.dart | 55 ++++++++++++++ 45 files changed, 224 insertions(+), 64 deletions(-) create mode 100644 lib/shared/widgets/querya_divider.dart create mode 100644 test/guards/no_raw_dividers_test.dart create mode 100644 test/shared/widgets/querya_divider_test.dart diff --git a/docs/ui-kit.md b/docs/ui-kit.md index 6664729c..e4118f93 100644 --- a/docs/ui-kit.md +++ b/docs/ui-kit.md @@ -218,6 +218,7 @@ Canonical labels and roles from the review: | shadcn `normal` / `small` size | `lg` (dialogs, forms) / `sm` or `md` (toolbars, by row) | | `QueryaActionButton` | `QueryaButton` `secondary`, `md` | | `QueryaActionMenu` trigger | `QueryaMenuButton` | +| `Divider` / `VerticalDivider` (Material or shadcn) | `QueryaDivider` / `QueryaDivider.vertical` (workbench `borderSubtle`; a guard rejects the raw ones) | | split "Execute" assembled by hand | `QueryaSplitButton` | | shadcn / material `IconButton`, private `_TreeIconButton` | `QueryaIconButton` (with tooltip) | | `InkWell` / `GestureDetector` around an icon or text | `QueryaButton` or `QueryaIconButton` | diff --git a/lib/features/command_palette/command_palette_dialog.dart b/lib/features/command_palette/command_palette_dialog.dart index c0263c59..d1b7dc06 100644 --- a/lib/features/command_palette/command_palette_dialog.dart +++ b/lib/features/command_palette/command_palette_dialog.dart @@ -201,7 +201,7 @@ class _CommandPaletteDialogState extends State { }, ), ), - const Divider(), + const QueryaDivider(), Expanded( child: _objectPrefix ? _PrefixHint(query: _objectQuery, onOpen: _openQuickSwitcher) diff --git a/lib/features/command_palette/quick_switcher_dialog.dart b/lib/features/command_palette/quick_switcher_dialog.dart index 7f869888..3ddacdf7 100644 --- a/lib/features/command_palette/quick_switcher_dialog.dart +++ b/lib/features/command_palette/quick_switcher_dialog.dart @@ -208,7 +208,7 @@ class _QuickSwitcherDialogState extends State { }, ), ), - const Divider(), + const QueryaDivider(), Expanded(child: _body(theme.colorScheme, hits, selectedIndex)), ], ), diff --git a/lib/features/connections/connections_panel.dart b/lib/features/connections/connections_panel.dart index 2b823f63..095190c5 100644 --- a/lib/features/connections/connections_panel.dart +++ b/lib/features/connections/connections_panel.dart @@ -1273,7 +1273,7 @@ class ConnectionsPanelState extends State { ), ), ), - Divider( + QueryaDivider( height: 1, color: theme.colorScheme.border.withValues(alpha: 0.22), ), diff --git a/lib/features/connections/driver_manager_dialog.dart b/lib/features/connections/driver_manager_dialog.dart index 40ddb41b..ce98561c 100644 --- a/lib/features/connections/driver_manager_dialog.dart +++ b/lib/features/connections/driver_manager_dialog.dart @@ -116,7 +116,7 @@ class _DriverManagerDialogContent extends material.StatelessWidget { shrinkWrap: true, padding: const material.EdgeInsets.symmetric(vertical: 8), itemCount: drivers.length, - separatorBuilder: (_, __) => material.Divider( + separatorBuilder: (_, __) => QueryaDivider( height: 1, color: theme.border.withValues(alpha: 0.3), ), diff --git a/lib/features/connections/sqlite_connection_form.dart b/lib/features/connections/sqlite_connection_form.dart index 7f005abb..e057d614 100644 --- a/lib/features/connections/sqlite_connection_form.dart +++ b/lib/features/connections/sqlite_connection_form.dart @@ -318,7 +318,7 @@ class _SqliteConnectionFormContentState ), ), ), - const material.Divider(height: 1), + const QueryaDivider(height: 1), // Test result banner if (_testResult != null) material.Padding( diff --git a/lib/features/extensions/extension_workspace_home.dart b/lib/features/extensions/extension_workspace_home.dart index 8149b7e0..142334a7 100644 --- a/lib/features/extensions/extension_workspace_home.dart +++ b/lib/features/extensions/extension_workspace_home.dart @@ -130,7 +130,7 @@ class _ExtensionWorkspaceHomeState ], ), ), - const Divider(height: 1), + const QueryaDivider(height: 1), Expanded( child: QueryaCrossFadeStack( index: _tab, diff --git a/lib/features/extensions/presentation/pages/extension_manager_dialog.dart b/lib/features/extensions/presentation/pages/extension_manager_dialog.dart index e8bf9d50..8494809e 100644 --- a/lib/features/extensions/presentation/pages/extension_manager_dialog.dart +++ b/lib/features/extensions/presentation/pages/extension_manager_dialog.dart @@ -254,7 +254,7 @@ class _ExtensionManagerContentState onSelected: (index) => setState(() => _tabIndex = index), ), ), - material.Divider(height: 1, color: theme.border), + QueryaDivider(height: 1, color: theme.border), material.Expanded( child: QueryaCrossFadeStack( index: _tabIndex, diff --git a/lib/features/main_screen/main_screen.dart b/lib/features/main_screen/main_screen.dart index c80f7c29..8a86649f 100644 --- a/lib/features/main_screen/main_screen.dart +++ b/lib/features/main_screen/main_screen.dart @@ -1118,7 +1118,7 @@ class _MainScreenState extends State { ); }, ), - Divider( + QueryaDivider( height: 1, color: wb.borderSubtle.withValues(alpha: 0.22)), Expanded( diff --git a/lib/features/mongodb/mongo_add_document_dialog.dart b/lib/features/mongodb/mongo_add_document_dialog.dart index 35c02e48..0e44a569 100644 --- a/lib/features/mongodb/mongo_add_document_dialog.dart +++ b/lib/features/mongodb/mongo_add_document_dialog.dart @@ -120,7 +120,7 @@ class _MongoAddDocumentDialogState extends material.State { : material.ListView.separated( cacheExtent: 400, itemCount: _collections.length, - separatorBuilder: (_, __) => Divider( + separatorBuilder: (_, __) => QueryaDivider( height: 1, color: cs.border.withValues(alpha: 0.15), ), diff --git a/lib/features/mongodb/mongo_create_index_dialog.dart b/lib/features/mongodb/mongo_create_index_dialog.dart index 72583fc9..1a894992 100644 --- a/lib/features/mongodb/mongo_create_index_dialog.dart +++ b/lib/features/mongodb/mongo_create_index_dialog.dart @@ -288,7 +288,7 @@ class _MongoCreateIndexDialogState ), ), const Gap(16), - const Divider(height: 1), + const QueryaDivider(height: 1), const Gap(12), // Options const Text('Index Options').semiBold().small(), diff --git a/lib/features/mongodb/mongo_database_dialog.dart b/lib/features/mongodb/mongo_database_dialog.dart index 84cc14ac..967b577d 100644 --- a/lib/features/mongodb/mongo_database_dialog.dart +++ b/lib/features/mongodb/mongo_database_dialog.dart @@ -71,7 +71,7 @@ class _CreateMongoDBDialogContentState ], ), ), - const material.Divider(height: 1), + const QueryaDivider(height: 1), material.Padding( padding: const material.EdgeInsets.all(24), child: material.Column( @@ -86,7 +86,7 @@ class _CreateMongoDBDialogContentState ], ), ), - const material.Divider(height: 1), + const QueryaDivider(height: 1), material.Container( padding: const material.EdgeInsets.symmetric( horizontal: 24, vertical: 16), diff --git a/lib/features/mongodb/mongo_databases_view.dart b/lib/features/mongodb/mongo_databases_view.dart index 3211d454..48e51ffe 100644 --- a/lib/features/mongodb/mongo_databases_view.dart +++ b/lib/features/mongodb/mongo_databases_view.dart @@ -340,7 +340,7 @@ class _MongoDatabasesViewState extends State { // Database rows for (var i = 0; i < _databases.length; i++) ...[ if (i > 0) - Divider(height: 1, color: cs.border.withValues(alpha: 0.15)), + QueryaDivider(height: 1, color: cs.border.withValues(alpha: 0.15)), _DatabaseRow( database: _databases[i], colorScheme: cs, diff --git a/lib/features/mongodb/mongo_document_tree_view.dart b/lib/features/mongodb/mongo_document_tree_view.dart index b1733359..20c1ef8a 100644 --- a/lib/features/mongodb/mongo_document_tree_view.dart +++ b/lib/features/mongodb/mongo_document_tree_view.dart @@ -9,6 +9,7 @@ import 'package:querya_desktop/shared/widgets/app_toast.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:querya_desktop/core/ui/querya_control_tokens.dart'; import 'package:querya_desktop/shared/widgets/querya_button.dart'; +import 'package:querya_desktop/shared/widgets/querya_divider.dart'; /// Appends a key or list index to a JavaScript / JSON Path expression. /// @@ -341,7 +342,7 @@ class _MongoDocumentTreeViewState extends State { ), // Expanded Tree content if (isExpanded) ...[ - material.Divider( + QueryaDivider( height: 1, color: cs.border.withValues(alpha: 0.2), ), diff --git a/lib/features/mongodb/mongo_documents_view.dart b/lib/features/mongodb/mongo_documents_view.dart index 5a86bcfb..8b232126 100644 --- a/lib/features/mongodb/mongo_documents_view.dart +++ b/lib/features/mongodb/mongo_documents_view.dart @@ -383,7 +383,7 @@ class _MongoDocumentsViewState extends material.State { children: [ // Filter bar _buildFilterBar(cs), - const Divider(height: 1), + const QueryaDivider(height: 1), // Error banner if (_error != null) _buildErrorBanner(cs), // Document content (mode-dependent) @@ -877,7 +877,7 @@ class MongoFieldList extends StatelessWidget { ); } - Widget _buildDivider() => material.Divider( + Widget _buildDivider() => QueryaDivider( height: 1, color: colorScheme.border.withValues(alpha: 0.2), ); diff --git a/lib/features/mongodb/mongo_explorer_view.dart b/lib/features/mongodb/mongo_explorer_view.dart index 22a4fca8..ab2d3c44 100644 --- a/lib/features/mongodb/mongo_explorer_view.dart +++ b/lib/features/mongodb/mongo_explorer_view.dart @@ -359,7 +359,7 @@ class _MongoExplorerViewState extends material.State { onRefresh: () => setState(() => _refreshToken++), onStats: () => setState(() => _showStats = !_showStats), ), - const Divider(height: 1), + const QueryaDivider(height: 1), // Content with fluid cross-fade morph between explorer and stats material.Expanded( child: QueryaSwitchingBody( diff --git a/lib/features/mongodb/mongo_indexes_view.dart b/lib/features/mongodb/mongo_indexes_view.dart index dfd13f04..679d6ec7 100644 --- a/lib/features/mongodb/mongo_indexes_view.dart +++ b/lib/features/mongodb/mongo_indexes_view.dart @@ -196,7 +196,7 @@ class _MongoIndexesViewState extends material.State { children: [ // Header action bar _buildHeader(cs), - const Divider(height: 1), + const QueryaDivider(height: 1), // Error banner if (_error != null) material.Container( diff --git a/lib/features/mongodb/mongo_query_workspace.dart b/lib/features/mongodb/mongo_query_workspace.dart index fdce9a4e..59a6cad6 100644 --- a/lib/features/mongodb/mongo_query_workspace.dart +++ b/lib/features/mongodb/mongo_query_workspace.dart @@ -232,7 +232,7 @@ class _MongoQueryWorkspaceState extends material.State { : material.ListView.separated( shrinkWrap: true, itemCount: history.length, - separatorBuilder: (_, __) => const Divider(height: 1), + separatorBuilder: (_, __) => const QueryaDivider(height: 1), itemBuilder: (context, i) { final item = history[i]; return material.ListTile( @@ -304,10 +304,10 @@ class _MongoQueryWorkspaceState extends material.State { children: [ // Top action bar _buildTopBar(cs), - const Divider(height: 1), + const QueryaDivider(height: 1), // Tab strip _buildTabStrip(cs), - const Divider(height: 1), + const QueryaDivider(height: 1), // Main content split material.Expanded( child: material.Column( @@ -326,10 +326,10 @@ class _MongoQueryWorkspaceState extends material.State { ), ), ), - const Divider(height: 1), + const QueryaDivider(height: 1), // Result bar & timing stats _buildResultBar(cs, tab), - const Divider(height: 1), + const QueryaDivider(height: 1), // Results view (bottom half) material.Expanded( flex: 6, diff --git a/lib/features/mongodb/mongo_stats_view.dart b/lib/features/mongodb/mongo_stats_view.dart index d70b82df..a5b698ca 100644 --- a/lib/features/mongodb/mongo_stats_view.dart +++ b/lib/features/mongodb/mongo_stats_view.dart @@ -222,7 +222,7 @@ class _MongoStatsViewState extends material.State { padding: const material.EdgeInsets.fromLTRB(24, 20, 24, 16), child: _header(context), ), - const Divider(height: 1), + const QueryaDivider(height: 1), material.Expanded( child: _buildBody(context, cs), ), diff --git a/lib/features/mongodb/mongodb_connection_form.dart b/lib/features/mongodb/mongodb_connection_form.dart index ed592cd3..965ebcdb 100644 --- a/lib/features/mongodb/mongodb_connection_form.dart +++ b/lib/features/mongodb/mongodb_connection_form.dart @@ -409,7 +409,7 @@ class _MongoConnectionFormContentState ], ), ), - const material.Divider(height: 1), + const QueryaDivider(height: 1), material.Expanded( child: material.FocusTraversalGroup( policy: material.WidgetOrderTraversalPolicy(), @@ -579,7 +579,7 @@ class _MongoConnectionFormContentState ), ), ), - const material.Divider(height: 1), + const QueryaDivider(height: 1), if (_testResult != null) material.Padding( padding: const material.EdgeInsets.fromLTRB(24, 8, 16, 8), diff --git a/lib/features/mysql/mysql_connection_form.dart b/lib/features/mysql/mysql_connection_form.dart index a0494b1a..85e7226f 100644 --- a/lib/features/mysql/mysql_connection_form.dart +++ b/lib/features/mysql/mysql_connection_form.dart @@ -383,7 +383,7 @@ class _MysqlConnectionFormContentState ], ), ), - const material.Divider(height: 1), + const QueryaDivider(height: 1), material.Expanded( child: material.FocusTraversalGroup( policy: material.WidgetOrderTraversalPolicy(), @@ -484,7 +484,7 @@ class _MysqlConnectionFormContentState ), ), ), - const material.Divider(height: 1), + const QueryaDivider(height: 1), if (_testResult != null) material.Padding( padding: const material.EdgeInsets.fromLTRB(24, 8, 16, 8), diff --git a/lib/features/mysql/mysql_stats_view.dart b/lib/features/mysql/mysql_stats_view.dart index 9181b5bd..f2132ac0 100644 --- a/lib/features/mysql/mysql_stats_view.dart +++ b/lib/features/mysql/mysql_stats_view.dart @@ -604,7 +604,7 @@ class _MysqlStatsViewState extends material.State { ], ), ), - material.Divider(height: 1, color: cs.border.withValues(alpha: 0.3)), + QueryaDivider(height: 1, color: cs.border.withValues(alpha: 0.3)), for (final db in databases) material.Padding( padding: const material.EdgeInsets.symmetric(vertical: 5), diff --git a/lib/features/mysql/mysql_workspace_home.dart b/lib/features/mysql/mysql_workspace_home.dart index d903e337..a1dab598 100644 --- a/lib/features/mysql/mysql_workspace_home.dart +++ b/lib/features/mysql/mysql_workspace_home.dart @@ -130,7 +130,7 @@ class _MysqlWorkspaceHomeState extends material.State { ], ), ), - const Divider(height: 1), + const QueryaDivider(height: 1), Expanded( child: QueryaCrossFadeStack( index: _tab, diff --git a/lib/features/onboarding/welcome_tour_dialog.dart b/lib/features/onboarding/welcome_tour_dialog.dart index 8883f82b..cd7a8759 100644 --- a/lib/features/onboarding/welcome_tour_dialog.dart +++ b/lib/features/onboarding/welcome_tour_dialog.dart @@ -674,15 +674,15 @@ class _HotkeysMatrixSlideView extends StatelessWidget { child: material.Column( children: [ _hotkeyRow(context, 'Run Query / Script', [cmdCtrl, 'Enter'], wb), - const material.Divider(height: 10), + const QueryaDivider(height: 10), _hotkeyRow(context, 'New Connection', [cmdCtrl, 'N'], wb), - const material.Divider(height: 10), + const QueryaDivider(height: 10), _hotkeyRow(context, 'Toggle Sidebar', [cmdCtrl, 'B'], wb), - const material.Divider(height: 10), + const QueryaDivider(height: 10), _hotkeyRow(context, 'Compound Filter Bar', [cmdCtrl, 'F'], wb), - const material.Divider(height: 10), + const QueryaDivider(height: 10), _hotkeyRow(context, 'Copy Selection (TSV)', [cmdCtrl, 'C'], wb), - const material.Divider(height: 10), + const QueryaDivider(height: 10), _hotkeyRow(context, 'Open Guide / Tour', ['F1'], wb), ], ), diff --git a/lib/features/postgresql/postgres_stats_view.dart b/lib/features/postgresql/postgres_stats_view.dart index aed1ee57..20fac0ac 100644 --- a/lib/features/postgresql/postgres_stats_view.dart +++ b/lib/features/postgresql/postgres_stats_view.dart @@ -566,7 +566,7 @@ class _PostgresStatsViewState extends material.State { ], ), ), - material.Divider(height: 1, color: cs.border.withValues(alpha: 0.3)), + QueryaDivider(height: 1, color: cs.border.withValues(alpha: 0.3)), for (final db in databases) material.Padding( padding: const material.EdgeInsets.symmetric(vertical: 5), diff --git a/lib/features/postgresql/postgres_table_privileges_dialog.dart b/lib/features/postgresql/postgres_table_privileges_dialog.dart index a26737a6..253d8210 100644 --- a/lib/features/postgresql/postgres_table_privileges_dialog.dart +++ b/lib/features/postgresql/postgres_table_privileges_dialog.dart @@ -133,9 +133,9 @@ class _PrivilegesDialogBodyState extends material.State<_PrivilegesDialogBody> { ], ), ), - const material.Divider(height: 1), + const QueryaDivider(height: 1), material.Expanded(child: _buildListArea(theme)), - const material.Divider(height: 1), + const QueryaDivider(height: 1), material.Padding( padding: const material.EdgeInsets.all(12), child: material.Row( diff --git a/lib/features/postgresql/postgres_workspace_home.dart b/lib/features/postgresql/postgres_workspace_home.dart index c17657fd..4c051c42 100644 --- a/lib/features/postgresql/postgres_workspace_home.dart +++ b/lib/features/postgresql/postgres_workspace_home.dart @@ -145,7 +145,7 @@ class _PostgresWorkspaceHomeState ], ), ), - const Divider(height: 1), + const QueryaDivider(height: 1), Expanded( child: QueryaCrossFadeStack( index: _tab, diff --git a/lib/features/postgresql/postgresql_connection_form.dart b/lib/features/postgresql/postgresql_connection_form.dart index c9bff6e1..a28a7f7b 100644 --- a/lib/features/postgresql/postgresql_connection_form.dart +++ b/lib/features/postgresql/postgresql_connection_form.dart @@ -536,7 +536,7 @@ class _PostgresConnectionFormContentState ], ), ), - const material.Divider(height: 1), + const QueryaDivider(height: 1), // Form body material.Expanded( child: material.FocusTraversalGroup( @@ -704,7 +704,7 @@ class _PostgresConnectionFormContentState ), ), ), - const material.Divider(height: 1), + const QueryaDivider(height: 1), // Test result banner if (_testResult != null) material.Padding( diff --git a/lib/features/redis/redis_connection_form.dart b/lib/features/redis/redis_connection_form.dart index c919f4c3..42e50281 100644 --- a/lib/features/redis/redis_connection_form.dart +++ b/lib/features/redis/redis_connection_form.dart @@ -338,7 +338,7 @@ class _RedisConnectionFormContentState ], ), ), - const material.Divider(height: 1), + const QueryaDivider(height: 1), material.Expanded( child: material.FocusTraversalGroup( policy: material.WidgetOrderTraversalPolicy(), @@ -498,7 +498,7 @@ class _RedisConnectionFormContentState ), ), ), - const material.Divider(height: 1), + const QueryaDivider(height: 1), if (_testResult != null) material.Padding( padding: const material.EdgeInsets.fromLTRB(24, 8, 16, 8), diff --git a/lib/features/redis/redis_explorer_view.dart b/lib/features/redis/redis_explorer_view.dart index 0a434502..7b8217ae 100644 --- a/lib/features/redis/redis_explorer_view.dart +++ b/lib/features/redis/redis_explorer_view.dart @@ -308,7 +308,7 @@ class _RedisExplorerViewState extends material.State { isStatsActive: _showStats, isReadOnly: widget.isReadOnly, ), - const Divider(height: 1), + const QueryaDivider(height: 1), // Content with fluid cross-fade morph between keys, stats and CLI material.Expanded( child: QueryaSwitchingBody( diff --git a/lib/features/redis/redis_key_editor.dart b/lib/features/redis/redis_key_editor.dart index 8ba2d83f..a8a72613 100644 --- a/lib/features/redis/redis_key_editor.dart +++ b/lib/features/redis/redis_key_editor.dart @@ -702,7 +702,7 @@ class _RedisKeyEditorState extends material.State { ), // Header _buildHeader(cs, shadcnCs), - const Divider(height: 1), + const QueryaDivider(height: 1), // Content material.Expanded( child: material.Padding( diff --git a/lib/features/redis/redis_keys_view.dart b/lib/features/redis/redis_keys_view.dart index 687e5d47..5363c5f1 100644 --- a/lib/features/redis/redis_keys_view.dart +++ b/lib/features/redis/redis_keys_view.dart @@ -436,7 +436,7 @@ class _RedisKeysViewState extends material.State { crossAxisAlignment: material.CrossAxisAlignment.stretch, children: [ _buildFilterBar(cs), - const Divider(height: 1), + const QueryaDivider(height: 1), if (_error != null) _buildErrorBanner(cs), material.Expanded(child: contentWidget), _buildStatusBar(cs), diff --git a/lib/features/redis/redis_view.dart b/lib/features/redis/redis_view.dart index 74958325..dcda93eb 100644 --- a/lib/features/redis/redis_view.dart +++ b/lib/features/redis/redis_view.dart @@ -231,7 +231,7 @@ class _RedisViewState extends material.State { padding: const material.EdgeInsets.fromLTRB(24, 20, 24, 16), child: _header(context), ), - const Divider(height: 1), + const QueryaDivider(height: 1), material.Expanded( child: _buildBody(context, cs), ), diff --git a/lib/features/settings/preferences_shortcuts_section.dart b/lib/features/settings/preferences_shortcuts_section.dart index 629b4b94..67da3443 100644 --- a/lib/features/settings/preferences_shortcuts_section.dart +++ b/lib/features/settings/preferences_shortcuts_section.dart @@ -343,7 +343,7 @@ class _PreferencesShortcutsSectionState children: [ for (var i = 0; i < entry.value.length; i++) ...[ if (i > 0) - Divider( + QueryaDivider( height: 1, color: theme.colorScheme.border.withValues(alpha: 0.18), ), diff --git a/lib/features/settings/theme_editor_dialog.dart b/lib/features/settings/theme_editor_dialog.dart index f74acc21..7dbee598 100644 --- a/lib/features/settings/theme_editor_dialog.dart +++ b/lib/features/settings/theme_editor_dialog.dart @@ -232,7 +232,7 @@ class _ThemeEditorDialogState extends material.State { i < themeEditorMvpColorFields.length; i++) ...[ if (i > 0) - Divider( + QueryaDivider( height: 1, color: cs.border.withValues(alpha: 0.18), ), diff --git a/lib/features/sqlite/sqlite_workspace_home.dart b/lib/features/sqlite/sqlite_workspace_home.dart index 122fb877..fc7090db 100644 --- a/lib/features/sqlite/sqlite_workspace_home.dart +++ b/lib/features/sqlite/sqlite_workspace_home.dart @@ -122,7 +122,7 @@ class _SqliteWorkspaceHomeState extends material.State { ], ), ), - const Divider(height: 1), + const QueryaDivider(height: 1), Expanded( child: QueryaCrossFadeStack( index: _tab, diff --git a/lib/features/workspace/data_grid_groupings_view.dart b/lib/features/workspace/data_grid_groupings_view.dart index df8b7c72..646726f1 100644 --- a/lib/features/workspace/data_grid_groupings_view.dart +++ b/lib/features/workspace/data_grid_groupings_view.dart @@ -161,7 +161,7 @@ class _DataGridGroupingsViewState ), const Gap(12), - material.VerticalDivider( + QueryaDivider.vertical( width: 1, thickness: 1, indent: 8, @@ -213,7 +213,7 @@ class _DataGridGroupingsViewState ], const Gap(12), - material.VerticalDivider( + QueryaDivider.vertical( width: 1, thickness: 1, indent: 8, @@ -275,7 +275,7 @@ class _DataGridGroupingsViewState material.Expanded( child: material.ListView.separated( itemCount: groups.length, - separatorBuilder: (_, __) => material.Divider( + separatorBuilder: (_, __) => QueryaDivider( height: 1, color: cs.border.withValues(alpha: 0.2), ), diff --git a/lib/features/workspace/generic_sql_workspace.dart b/lib/features/workspace/generic_sql_workspace.dart index ede24c15..ec5c8c8c 100644 --- a/lib/features/workspace/generic_sql_workspace.dart +++ b/lib/features/workspace/generic_sql_workspace.dart @@ -1262,7 +1262,7 @@ class GenericSqlWorkspaceState extends material.State { onTimeoutChanged: _onStmtTimeoutChanged, ), ), - const Divider(height: 1), + const QueryaDivider(height: 1), Expanded( child: QueryEditorTab( controller: session.controller, diff --git a/lib/features/workspace/sql_query_history_dialog.dart b/lib/features/workspace/sql_query_history_dialog.dart index ae8134bd..69ec52b3 100644 --- a/lib/features/workspace/sql_query_history_dialog.dart +++ b/lib/features/workspace/sql_query_history_dialog.dart @@ -317,7 +317,7 @@ class _SqlQueryHistoryDialogContentState ), itemCount: items.length, separatorBuilder: (_, __) => - material.Divider(height: 1, color: scheme.border), + QueryaDivider(height: 1, color: scheme.border), itemBuilder: (context, i) { final e = items[i]; final when = _formatWhen(e.recordedAt); diff --git a/lib/shared/widgets/querya_divider.dart b/lib/shared/widgets/querya_divider.dart new file mode 100644 index 00000000..9574b784 --- /dev/null +++ b/lib/shared/widgets/querya_divider.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart' as material; +import 'package:querya_desktop/core/theme/querya_theme_scope.dart'; + +/// A hairline rule in the workbench border colour (#1443). +/// +/// Replaces Material's `Divider` / `VerticalDivider`, which read the Material +/// `dividerColor` and come out near-black in the light theme. Without a +/// [color] the rule is `workbench.borderSubtle`; [height] (or [width] for +/// [QueryaDivider.vertical]) is the space the rule occupies, [thickness] the +/// line itself. +class QueryaDivider extends material.StatelessWidget { + /// A horizontal rule, [height] tall (default 1). + const QueryaDivider({ + super.key, + this.height = 1, + this.thickness = 1, + this.indent = 0, + this.endIndent = 0, + this.color, + }) : vertical = false; + + /// A vertical rule, [width] wide (passed as `height` of the shared fields). + const QueryaDivider.vertical({ + super.key, + double width = 1, + this.thickness = 1, + this.indent = 0, + this.endIndent = 0, + this.color, + }) : height = width, + vertical = true; + + final double height; + final double thickness; + + /// Space before the rule (left / top). + final double indent; + + /// Space after the rule (right / bottom). + final double endIndent; + + /// Overrides `workbench.borderSubtle`. + final material.Color? color; + + /// Whether the rule is vertical (`.vertical`), [height] is then its width. + final bool vertical; + + @override + material.Widget build(material.BuildContext context) { + final ruleColor = color ?? context.workbenchOrDefault.borderSubtle; + final line = material.ColoredBox(color: ruleColor); + if (vertical) { + return material.SizedBox( + width: height, + child: material.Center( + child: material.Padding( + padding: material.EdgeInsets.only(top: indent, bottom: endIndent), + child: material.SizedBox(width: thickness, child: line), + ), + ), + ); + } + return material.SizedBox( + height: height, + child: material.Center( + child: material.Padding( + padding: material.EdgeInsets.only(left: indent, right: endIndent), + child: material.SizedBox(height: thickness, child: line), + ), + ), + ); + } +} diff --git a/lib/shared/widgets/widgets.dart b/lib/shared/widgets/widgets.dart index 0ee98575..cc1a9091 100644 --- a/lib/shared/widgets/widgets.dart +++ b/lib/shared/widgets/widgets.dart @@ -5,7 +5,7 @@ /// - Card, PrimaryButton, OutlineButton, GhostButton /// - Tabs, TabItem, IndexedStack /// - TextField, Text (with .semiBold(), .muted(), .small()) -/// - Gap, VerticalDivider +/// - Gap library; export 'app_dialog.dart'; @@ -25,6 +25,7 @@ export 'querya_action_menu.dart' show QueryaActionMenuItem; export 'querya_badge.dart'; export 'querya_button.dart'; export 'querya_dialog_actions.dart'; +export 'querya_divider.dart'; export 'querya_menu_button.dart'; export 'querya_split_button.dart'; export 'querya_empty_state.dart'; diff --git a/test/guards/no_raw_dividers_test.dart b/test/guards/no_raw_dividers_test.dart new file mode 100644 index 00000000..c38b477e --- /dev/null +++ b/test/guards/no_raw_dividers_test.dart @@ -0,0 +1,29 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +/// #1443: Material's `Divider` reads the Material `dividerColor` and is +/// near-black in the light theme. Rules in `lib/` go through `QueryaDivider` +/// (`lib/shared/widgets/querya_divider.dart`). +void main() { + test('no raw Divider / VerticalDivider in lib/ outside of QueryaDivider', () { + final banned = RegExp(r'(?[]; + + for (final f in Directory('lib').listSync(recursive: true)) { + if (f is! File || !f.path.endsWith('.dart')) continue; + if (f.path.endsWith('lib/shared/widgets/querya_divider.dart')) continue; + + if (banned.hasMatch(f.readAsStringSync())) { + offenders.add(f.path); + } + } + + expect( + offenders, + isEmpty, + reason: 'Found raw Divider / VerticalDivider in: $offenders. ' + 'Use QueryaDivider from lib/shared/widgets/widgets.dart instead.', + ); + }); +} diff --git a/test/shared/widgets/querya_divider_test.dart b/test/shared/widgets/querya_divider_test.dart new file mode 100644 index 00000000..efd5bffe --- /dev/null +++ b/test/shared/widgets/querya_divider_test.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart' as material; +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/shared/widgets/querya_divider.dart'; + +import '../../support/querya_theme_test_shell.dart'; + +void main() { + group('QueryaDivider', () { + testWidgets('a horizontal rule is as tall as its height', (tester) async { + await tester.pumpWidget( + queryaThemeTestShell( + child: const material.Column( + children: [QueryaDivider(height: 10)], + ), + ), + ); + + expect(tester.getSize(find.byType(QueryaDivider)).height, 10); + }); + + testWidgets('a vertical rule is as wide as its width', (tester) async { + await tester.pumpWidget( + queryaThemeTestShell( + child: const material.SizedBox( + height: 40, + child: material.Row( + children: [QueryaDivider.vertical(width: 6)], + ), + ), + ), + ); + + expect(tester.getSize(find.byType(QueryaDivider)).width, 6); + }); + + testWidgets('an explicit colour wins over the workbench border', + (tester) async { + await tester.pumpWidget( + queryaThemeTestShell( + child: const material.Column( + children: [QueryaDivider(color: material.Color(0xFFFF0000))], + ), + ), + ); + + final box = tester.widget( + find.descendant( + of: find.byType(QueryaDivider), + matching: find.byType(material.ColoredBox), + ), + ); + expect(box.color, const material.Color(0xFFFF0000)); + }); + }); +}