From e7cf47cb2d813378062c4ef392ff7bc05ae9070a Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 11 Oct 2026 07:37:34 +0300 Subject: [PATCH 1/2] refactor(ui): QueryaModalDialog and QueryaConfirmDialog on QueryaButton, off shadcn The confirm dialog footer is a ghost Cancel and a primary / destructive main action at lg, the close icon is a QueryaButton.icon, gaps are SizedBoxes and the colour scheme comes from context.colors. The module leaves the shadcn allow-list; the two tests that looked for DestructiveButton look for a destructive QueryaButton. Part of #1338 and #1365. --- lib/shared/widgets/querya_modal_dialog.dart | 80 ++++++++++--------- .../shared_does_not_import_shadcn_test.dart | 1 - .../widgets/querya_modal_dialog_test.dart | 8 +- .../widgets/querya_ui_kit_behavior_test.dart | 8 +- 4 files changed, 57 insertions(+), 40 deletions(-) diff --git a/lib/shared/widgets/querya_modal_dialog.dart b/lib/shared/widgets/querya_modal_dialog.dart index d4561a5f..a551cdea 100644 --- a/lib/shared/widgets/querya_modal_dialog.dart +++ b/lib/shared/widgets/querya_modal_dialog.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart' as material; -import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:querya_desktop/core/theme/querya_theme_scope.dart'; +import 'package:querya_desktop/core/ui/querya_control_tokens.dart'; import 'app_dialog.dart'; +import 'querya_button.dart'; import 'querya_dialog_card.dart'; /// Standard modal dialog shell with title, optional description, icon, content, and actions. @@ -53,7 +54,7 @@ class QueryaModalDialog extends material.StatelessWidget { @override material.Widget build(material.BuildContext context) { - final cs = Theme.of(context).colorScheme; + final cs = context.colors; final workbench = QueryaThemeScope.maybeOf(context)?.workbench; material.Widget? leadingIcon = icon; @@ -93,7 +94,7 @@ class QueryaModalDialog extends material.StatelessWidget { children: [ if (leadingIcon != null) ...[ leadingIcon, - const Gap(14), + const material.SizedBox(width: 14), ], material.Expanded( child: material.Column( @@ -109,7 +110,7 @@ class QueryaModalDialog extends material.StatelessWidget { child: title, ), if (description != null) ...[ - const Gap(4), + const material.SizedBox(height: 4), material.DefaultTextStyle( style: material.TextStyle( fontSize: 13, @@ -124,17 +125,12 @@ class QueryaModalDialog extends material.StatelessWidget { if (showCloseButton || onClose != null) material.Padding( padding: const material.EdgeInsets.only(left: 8), - child: material.IconButton( - icon: const material.Icon(material.Icons.close_rounded, size: 18), - splashRadius: 16, - padding: material.EdgeInsets.zero, - constraints: const material.BoxConstraints( - minWidth: 28, - minHeight: 28, - ), - color: cs.mutedForeground, - hoverColor: cs.muted.withValues(alpha: 0.3), - onPressed: onClose ?? () => Navigator.of(context).pop(), + child: QueryaButton.icon( + icon: material.Icons.close_rounded, + tooltip: 'Close', + size: QueryaControlSize.sm, + onPressed: + onClose ?? () => material.Navigator.of(context).pop(), ), ), ], @@ -142,21 +138,25 @@ class QueryaModalDialog extends material.StatelessWidget { // Content if (content != null) ...[ - const Gap(16), + const material.SizedBox(height: 16), content!, ], // Actions if (actions != null && actions!.isNotEmpty) ...[ - const Gap(20), + const material.SizedBox(height: 20), material.Align( alignment: material.Alignment.centerRight, - child: material.Wrap( - alignment: material.WrapAlignment.end, - crossAxisAlignment: material.WrapCrossAlignment.center, - spacing: 8, - runSpacing: 8, - children: actions!, + // Buttons of the footer without an explicit size are `lg`. + child: QueryaControlScope( + size: QueryaControlSize.lg, + child: material.Wrap( + alignment: material.WrapAlignment.end, + crossAxisAlignment: material.WrapCrossAlignment.center, + spacing: 8, + runSpacing: 8, + children: actions!, + ), ), ), ], @@ -211,15 +211,15 @@ class QueryaConfirmDialog extends material.StatelessWidget { cancelLabel: cancelLabel, isDestructive: isDestructive, icon: icon, - onConfirm: () => Navigator.of(ctx).pop(true), - onCancel: () => Navigator.of(ctx).pop(false), + onConfirm: () => material.Navigator.of(ctx).pop(true), + onCancel: () => material.Navigator.of(ctx).pop(false), ), ); } @override material.Widget build(material.BuildContext context) { - final cs = Theme.of(context).colorScheme; + final cs = context.colors; final workbench = QueryaThemeScope.maybeOf(context)?.workbench; final effectiveIconData = icon ?? @@ -232,24 +232,30 @@ class QueryaConfirmDialog extends material.StatelessWidget { : (workbench?.accent ?? cs.primary); return QueryaModalDialog( - title: Text(title), - description: Text(message), + title: material.Text(title), + description: material.Text(message), icon: material.Icon(effectiveIconData), iconColor: iconColor, actions: [ - OutlineButton( - onPressed: onCancel ?? () => Navigator.of(context).pop(false), - child: Text(cancelLabel), + QueryaButton.ghost( + label: cancelLabel, + size: QueryaControlSize.lg, + onPressed: + onCancel ?? () => material.Navigator.of(context).pop(false), ), if (isDestructive) - DestructiveButton( - onPressed: onConfirm ?? () => Navigator.of(context).pop(true), - child: Text(confirmLabel), + QueryaButton.destructive( + label: confirmLabel, + size: QueryaControlSize.lg, + onPressed: + onConfirm ?? () => material.Navigator.of(context).pop(true), ) else - PrimaryButton( - onPressed: onConfirm ?? () => Navigator.of(context).pop(true), - child: Text(confirmLabel), + QueryaButton.primary( + label: confirmLabel, + size: QueryaControlSize.lg, + onPressed: + onConfirm ?? () => material.Navigator.of(context).pop(true), ), ], ); diff --git a/test/guards/shared_does_not_import_shadcn_test.dart b/test/guards/shared_does_not_import_shadcn_test.dart index d432c31b..299bc801 100644 --- a/test/guards/shared_does_not_import_shadcn_test.dart +++ b/test/guards/shared_does_not_import_shadcn_test.dart @@ -16,7 +16,6 @@ void main() { 'lib/shared/widgets/querya_action_button.dart', 'lib/shared/widgets/querya_action_menu.dart', 'lib/shared/widgets/querya_empty_state.dart', - 'lib/shared/widgets/querya_modal_dialog.dart', 'lib/shared/widgets/widgets.dart', }; diff --git a/test/shared/widgets/querya_modal_dialog_test.dart b/test/shared/widgets/querya_modal_dialog_test.dart index 9e1386a2..1c77a6c2 100644 --- a/test/shared/widgets/querya_modal_dialog_test.dart +++ b/test/shared/widgets/querya_modal_dialog_test.dart @@ -99,7 +99,13 @@ void main() { ), ); - expect(find.byType(DestructiveButton), findsOneWidget); + expect( + find.byWidgetPredicate( + (w) => + w is QueryaButton && w.variant == QueryaButtonVariant.destructive, + ), + findsOneWidget, + ); expect(find.text('Drop'), findsOneWidget); }); }); diff --git a/test/shared/widgets/querya_ui_kit_behavior_test.dart b/test/shared/widgets/querya_ui_kit_behavior_test.dart index c0661266..2ff2dd15 100644 --- a/test/shared/widgets/querya_ui_kit_behavior_test.dart +++ b/test/shared/widgets/querya_ui_kit_behavior_test.dart @@ -252,7 +252,13 @@ void main() { (tester) async { await open(tester, (_) {}, isDestructive: true); - expect(find.byType(DestructiveButton), findsOneWidget); + expect( + find.byWidgetPredicate( + (w) => + w is QueryaButton && w.variant == QueryaButtonVariant.destructive, + ), + findsOneWidget, + ); }); }); } From 74b2e5f71603cf52a4394b5826ded85d13b4f3f3 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 11 Oct 2026 07:43:21 +0300 Subject: [PATCH 2/2] fix(ui): kit buttons read the workbench with a default when there is no scope QueryaButton asserted on a missing QueryaThemeScope, which dialogs built under a bare app (and their tests) do not have. workbenchOrDefault falls back to the defaults for the ambient brightness; the scheme comes from context.colors. --- lib/core/theme/querya_theme_scope.dart | 10 ++++++++++ lib/shared/widgets/querya_button.dart | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/core/theme/querya_theme_scope.dart b/lib/core/theme/querya_theme_scope.dart index c05e451f..58067fd6 100644 --- a/lib/core/theme/querya_theme_scope.dart +++ b/lib/core/theme/querya_theme_scope.dart @@ -60,6 +60,16 @@ extension QueryaThemeContext on BuildContext { QueryaWorkbenchTheme get workbench => queryaTheme.workbench; + /// The workbench tokens of the nearest [QueryaThemeScope], or the defaults + /// for the ambient brightness when there is none (a widget under a bare app, + /// as in tests). Kit controls use this so they never assert on a missing + /// scope. + QueryaWorkbenchTheme get workbenchOrDefault => + QueryaThemeScope.maybeOf(this)?.workbench ?? + (colors.brightness == Brightness.light + ? QueryaWorkbenchTheme.lightDefault + : QueryaWorkbenchTheme.darkDefault); + QueryaEditorTheme get editorTheme => queryaTheme.editor; QueryaSemanticPalette get semanticPalette => diff --git a/lib/shared/widgets/querya_button.dart b/lib/shared/widgets/querya_button.dart index 1bd11848..08dd8595 100644 --- a/lib/shared/widgets/querya_button.dart +++ b/lib/shared/widgets/querya_button.dart @@ -294,8 +294,8 @@ class _QueryaButtonState extends material.State { @override material.Widget build(material.BuildContext context) { final w = widget; - final wb = context.workbench; - final scheme = context.queryaTheme.colorScheme; + final wb = context.workbenchOrDefault; + final scheme = context.colors; final size = w.size ?? QueryaControlSize.of(context); final enabled = _enabled; final hasLabel = w.label != null;