diff --git a/lib/features/workspace/sql_query_tab_bar.dart b/lib/features/workspace/sql_query_tab_bar.dart index a77e5dd7..f40479cd 100644 --- a/lib/features/workspace/sql_query_tab_bar.dart +++ b/lib/features/workspace/sql_query_tab_bar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart' as material; +import 'package:querya_desktop/features/workspace/sql_editor_chrome.dart'; import 'package:querya_desktop/features/workspace/sql_query_tab_session.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; import 'package:querya_desktop/core/ui/querya_control_tokens.dart'; @@ -26,18 +27,11 @@ class SqlQueryTabBar extends material.StatelessWidget { @override material.Widget build(material.BuildContext context) { - final theme = Theme.of(context); - final cs = theme.colorScheme; - + // The same strip as the toolbar under it: workbench surface, hairline. return material.Container( height: 40, padding: const material.EdgeInsets.symmetric(horizontal: 8), - decoration: material.BoxDecoration( - color: cs.card, - border: material.Border( - bottom: material.BorderSide(color: cs.border), - ), - ), + decoration: SqlEditorChrome.sqlToolbarDecoration(context), child: material.Row( children: [ material.Expanded( diff --git a/lib/features/workspace/sql_workspace_toolbar.dart b/lib/features/workspace/sql_workspace_toolbar.dart index 8de9d253..444c99fe 100644 --- a/lib/features/workspace/sql_workspace_toolbar.dart +++ b/lib/features/workspace/sql_workspace_toolbar.dart @@ -82,7 +82,10 @@ class SqlWorkspaceToolbar extends StatelessWidget { final VoidCallback onToggleAutocommit; final void Function(int? seconds) onTimeoutChanged; - static const double compactWidthBreakpoint = 900; + /// Below this width only the run button keeps its label; the rest are + /// icons. Below [iconOnlyWidth] the run button is an icon too. + static const double labelsWidth = 900; + static const double iconOnlyWidth = 620; static String get _modLabel => Platform.isMacOS ? '⌘' : 'Ctrl+'; List> _sessionMenuItems() => [ @@ -125,27 +128,33 @@ class SqlWorkspaceToolbar extends StatelessWidget { } } - material.Widget _txBadge() { - final open = txOpen; - // An unknown state (before the first run) reads as the commit mode, not - // as a dash. - if (open == true) { + /// What is not the default: an open transaction or manual commit. Auto-commit + /// is the default and is named by the Session menu, so it has no badge of its + /// own next to it (#1558). + material.Widget? _txBadge() { + if (txOpen == true) { return const QueryaBadge.status( 'Transaction open', status: QueryaBadgeStatus.warning, ); } - return QueryaBadge.status( - autocommit ? 'Auto-commit' : 'Manual commit', - status: autocommit ? QueryaBadgeStatus.neutral : QueryaBadgeStatus.info, - ); + if (!autocommit) { + return const QueryaBadge.status( + 'Manual commit', + status: QueryaBadgeStatus.info, + ); + } + return null; } @override material.Widget build(material.BuildContext context) { return material.LayoutBuilder( builder: (context, constraints) { - final compact = constraints.maxWidth < compactWidthBreakpoint; + // Controls give up their labels as the row narrows, the run button + // last, so the row stays on one line without hiding what it does. + final compact = constraints.maxWidth < labelsWidth; + final runCompact = constraints.maxWidth < iconOnlyWidth; // One size for the whole row: the run split button, the buttons, the // icon buttons and the session menu are all `md` (#1439). return QueryaControlScope( @@ -163,7 +172,7 @@ class SqlWorkspaceToolbar extends StatelessWidget { menuKey: const material.ValueKey('run_menu'), label: 'Execute (F5)', icon: material.Icons.play_arrow_rounded, - compact: compact, + compact: runCompact, loading: session.running, onPressed: () => onExecute(false), menuTooltip: 'Run options', @@ -187,10 +196,11 @@ class SqlWorkspaceToolbar extends StatelessWidget { ), if (delegate.supportsExplain) ...[ const material.SizedBox(width: 8), - QueryaButton.secondary( + // A magnifier over lines: the plan. The diagram keeps the tree. + QueryaButton.ghost( key: const material.ValueKey('explain_query'), label: 'Explain', - icon: material.Icons.account_tree_outlined, + icon: material.Icons.manage_search_rounded, compact: compact, tooltip: 'Show the query plan', onPressed: session.running ? null : onExplain, @@ -242,7 +252,7 @@ class SqlWorkspaceToolbar extends StatelessWidget { const material.SizedBox(width: 8), ], if (delegate.supportsTransactions) ...[ - _txBadge(), + if (_txBadge() case final badge?) badge, if (txOpen == true) ...[ const material.SizedBox(width: 8), QueryaButton.secondary( @@ -262,10 +272,14 @@ class SqlWorkspaceToolbar extends StatelessWidget { ], QueryaMenuButton( key: const material.ValueKey('session_menu'), + variant: QueryaButtonVariant.ghost, items: _sessionMenuItems(), onSelected: (choice) => _onSessionChoice(context, choice), enabled: !session.running, icon: material.Icons.tune_rounded, + tooltip: autocommit + ? 'Session: auto-commit' + : 'Session: manual commit', label: compact ? null : (autocommit ? 'Session · auto-commit' : 'Session · manual'), diff --git a/test/e2e/02_sql_workspaces/e2e_transaction_guard_test.dart b/test/e2e/02_sql_workspaces/e2e_transaction_guard_test.dart index aa8b1bd6..32032e11 100644 --- a/test/e2e/02_sql_workspaces/e2e_transaction_guard_test.dart +++ b/test/e2e/02_sql_workspaces/e2e_transaction_guard_test.dart @@ -55,19 +55,23 @@ void main() { final delegate = _TxDelegate(); await ws.pump(tester, delegate); await E2eSqlWorkspace.settle(tester); - // Unknown until the first command refreshes the state. - expect(find.text('Auto-commit'), findsOneWidget); + // Unknown until the first command refreshes the state: the default, + // which has no badge; the Session menu names it. + expect(find.text('Transaction open'), findsNothing); + expect(find.byTooltip('Session: auto-commit'), findsOneWidget); await beginFromSessionMenu(tester); expect(find.text('Transaction open'), findsOneWidget); expect(find.text('OK: BEGIN'), findsWidgets); await tapAndSettle(tester, 'Rollback'); - expect(find.text('Auto-commit'), findsOneWidget); + expect(find.text('Transaction open'), findsNothing); + expect(find.byTooltip('Session: auto-commit'), findsOneWidget); await beginFromSessionMenu(tester); await tapAndSettle(tester, 'Commit'); - expect(find.text('Auto-commit'), findsOneWidget); + expect(find.text('Transaction open'), findsNothing); + expect(find.byTooltip('Session: auto-commit'), findsOneWidget); expect(delegate.commands, ['BEGIN', 'ROLLBACK', 'BEGIN', 'COMMIT']); }); diff --git a/test/features/workspace/generic_sql_workspace_shortcuts_test.dart b/test/features/workspace/generic_sql_workspace_shortcuts_test.dart index fcce2173..a60feb5b 100644 --- a/test/features/workspace/generic_sql_workspace_shortcuts_test.dart +++ b/test/features/workspace/generic_sql_workspace_shortcuts_test.dart @@ -367,20 +367,30 @@ void main() { expect(tester.getCenter(session).dy, closeTo(tester.getCenter(run).dy, 1)); }); - testWidgets('below the breakpoint the labels hide and the icons stay', + testWidgets('the row gives up its labels one control at a time (#1558)', timeout: _timeout, (tester) async { await pumpWorkspace(tester, FakeSqlExecutionDelegate()); expect(find.text('Execute (F5)'), findsOneWidget); expect(find.text('Explain'), findsOneWidget); + expect(find.text('Session · auto-commit'), findsOneWidget); + // Narrower: Explain and Session become icons, the run button keeps its + // label. await tester.binding.setSurfaceSize(const material.Size(800, 700)); await tester.pumpAndSettle(); + expect(find.text('Execute (F5)'), findsOneWidget); + expect(find.text('Explain'), findsNothing); + expect(find.text('Session · auto-commit'), findsNothing); + expect(find.byKey(const material.ValueKey('explain_query')), findsOneWidget); + expect(find.byTooltip('Show the query plan'), findsOneWidget); + expect(find.byTooltip('Session: auto-commit'), findsOneWidget); + // Narrowest: the run button is an icon too, and every control stays. + await tester.binding.setSurfaceSize(const material.Size(560, 700)); + await tester.pumpAndSettle(); expect(find.text('Execute (F5)'), findsNothing); - expect(find.text('Explain'), findsNothing); expect(find.byKey(const material.ValueKey('run_script')), findsOneWidget); expect(find.byKey(const material.ValueKey('explain_query')), findsOneWidget); - expect(find.byTooltip('Show the query plan'), findsOneWidget); expect(find.byTooltip('Execute (F5)'), findsOneWidget); }); diff --git a/test/guards/sql_workspace_chrome_guards_test.dart b/test/guards/sql_workspace_chrome_guards_test.dart new file mode 100644 index 00000000..30300ba2 --- /dev/null +++ b/test/guards/sql_workspace_chrome_guards_test.dart @@ -0,0 +1,51 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +/// The bars around the SQL editor are one visual system (#1558): they take +/// their colours from the workbench tokens, and no two controls of the row +/// share an icon. +void main() { + const chrome = [ + 'lib/features/workspace/sql_query_tab_bar.dart', + 'lib/features/workspace/sql_workspace_toolbar.dart', + 'lib/features/workspace/sql_editor_chrome.dart', + ]; + + String code(String path) => File(path) + .readAsLinesSync() + .where((l) => !l.trimLeft().startsWith('//')) + .join('\n'); + + test('the SQL pane chrome uses workbench tokens, not the shadcn scheme', () { + for (final path in chrome) { + final source = code(path); + expect(source.contains('colorScheme'), isFalse, + reason: '$path reads Theme.colorScheme: use context.workbench.'); + expect(RegExp(r'Color\(0x').hasMatch(source), isFalse, + reason: '$path has a raw colour: use a theme token.'); + } + }); + + test('Explain and the Diagram button do not share an icon', () { + final toolbar = code('lib/features/workspace/sql_workspace_toolbar.dart'); + final workspace = code('lib/features/workspace/generic_sql_workspace.dart'); + final explain = RegExp( + r"ValueKey\('explain_query'\)[\s\S]*?icon: material\.Icons\.(\w+)", + ).firstMatch(toolbar)?.group(1); + final diagram = RegExp( + r"ValueKey\('open_diagram_tab'\)[\s\S]*?icon: material\.Icons\.(\w+)", + ).firstMatch(workspace)?.group(1); + + expect(explain, isNotNull); + expect(diagram, isNotNull); + expect(explain, isNot(diagram)); + }); + + test('the tab strip and the toolbar share one surface decoration', () { + expect(code('lib/features/workspace/sql_query_tab_bar.dart'), + contains('SqlEditorChrome.sqlToolbarDecoration')); + expect(code('lib/features/workspace/generic_sql_workspace.dart'), + contains('SqlEditorChrome.sqlToolbarDecoration')); + }); +}