From b9bd6a7547412c704ddd52e2c0f2f69e3b50c092 Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Tue, 21 Jul 2026 00:57:54 +0300 Subject: [PATCH 1/2] fix(routing): treat a blank title suffix as absent so tab titles have no dangling separator --- lib/src/routing/title_manager.dart | 25 +++++++++++++++++-------- test/routing/title_manager_test.dart | 16 ++++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/lib/src/routing/title_manager.dart b/lib/src/routing/title_manager.dart index bc5a6ed..6e4e181 100644 --- a/lib/src/routing/title_manager.dart +++ b/lib/src/routing/title_manager.dart @@ -132,29 +132,38 @@ class TitleManager { /// /// The resolved title is passed through [trans] lazily at read time (a plain /// literal passes through unchanged); the suffix stays literal and is never - /// translated. When an override or route title is active and a suffix is - /// present, the result is `"$title$_separator$suffix"`. When the resolved - /// title falls back to the application title, the suffix is omitted. When no - /// title is available, an empty string is returned. + /// translated. When an override or route title is active and a NON-EMPTY + /// suffix is present, the result is `"$title$_separator$suffix"`. A null or + /// blank suffix (e.g. an unset `APP_NAME`) is treated as absent, so the title + /// never trails a dangling separator (`"Home | "`) or an empty suffix. When + /// the resolved title falls back to the application title, the suffix is + /// omitted. When no title is available, an empty string is returned. String get effectiveTitle { final overrideTitle = _overrideTitle; if (overrideTitle != null) { final resolved = trans(overrideTitle); - final suffix = _suffix; - return suffix != null ? '$resolved$_separator$suffix' : resolved; + return _withSuffix(resolved); } final routeTitle = _routeTitle; if (routeTitle != null) { final resolved = trans(routeTitle); - final suffix = _suffix; - return suffix != null ? '$resolved$_separator$suffix' : resolved; + return _withSuffix(resolved); } final appTitle = _appTitle; return appTitle != null ? trans(appTitle) : ''; } + /// Glues the [_suffix] onto [title] with the [_separator], or returns [title] + /// unchanged when the suffix is null or blank (so a missing app name never + /// leaves a dangling `"$title$separator"`). + String _withSuffix(String title) { + final suffix = _suffix; + if (suffix == null || suffix.trim().isEmpty) return title; + return '$title$_separator$suffix'; + } + // --------------------------------------------------------------------------- // Reset (Testing) // --------------------------------------------------------------------------- diff --git a/test/routing/title_manager_test.dart b/test/routing/title_manager_test.dart index c1d7d7d..e53d27b 100644 --- a/test/routing/title_manager_test.dart +++ b/test/routing/title_manager_test.dart @@ -100,6 +100,22 @@ void main() { expect(TitleManager.instance.effectiveTitle, 'App'); }); + test('a blank suffix is treated as absent (no dangling separator)', () { + // A misconfigured app name (empty or whitespace, e.g. an unset + // APP_NAME) must not leave a trailing separator like "Dashboard - " on + // the route title. + TitleManager.configure(); + TitleManager.instance + ..setAppTitle('App') + ..setRouteTitle('Dashboard') + ..setSuffix(''); + + expect(TitleManager.instance.effectiveTitle, 'Dashboard'); + + TitleManager.instance.setSuffix(' '); + expect(TitleManager.instance.effectiveTitle, 'Dashboard'); + }); + test( 'suffix not applied when raw title is null — returns empty string', () { From 60fafc63c354128ef851346d37139a03a3ba2d91 Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Tue, 21 Jul 2026 10:11:10 +0300 Subject: [PATCH 2/2] fix(feedback): wrap overlay toast in transparent Material, drop unused color params, and guard degrade-path logging --- CHANGELOG.md | 2 ++ lib/src/ui/magic_feedback.dart | 39 ++++++++++++++++++++++++++------ test/ui/magic_feedback_test.dart | 11 +++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1ec5c6..62900ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ All notable changes to this project will be documented in this file. ### Fixed - **`MagicFeedback` toasts now show in Scaffold-less (Wind-only) views instead of throwing or silently doing nothing.** `Magic.error` / `Magic.success` / `MagicFeedback.info` routed through `ScaffoldMessenger.of(context).showSnackBar`, which asserts `_scaffolds.isNotEmpty` when no Material `Scaffold` hosts the view. In a Wind-built screen (no `Scaffold`) that assertion escaped the caller's own `try/catch` and stalled the flow. Toast delivery now goes through the Navigator overlay, read from `navigatorKey.currentState.overlay` (NOT `Overlay.maybeOf`, which sits above that overlay), as a single non-interactive auto-dismissing bottom entry that replaces the previous one and degrades to a logged warning when no overlay is available (never throwing). The `Magic.error` / `success` / `toast` API surface is unchanged; only the delivery path is. Touches `lib/src/ui/magic_feedback.dart`; covered by `test/ui/magic_feedback_test.dart`. +- **`MagicFeedback` overlay toasts render clean text and degrade without throwing.** The overlay toast content was not wrapped in a `Material`, so its text inherited the root fallback `DefaultTextStyle` (the yellow debug double-underline); it now sits under a transparent `Material`, matching the dialog / loading builders. The unused `backgroundColor` / `color` parameters on `showSnackbar` (the overlay path never applied them) are removed, and the degrade-path warnings now log through `Log` only when the `log` service is bound (falling back to `debugPrint`), so feedback triggered before `Magic.init` binds logging degrades instead of throwing `Service [log] is not registered`. Touches `lib/src/ui/magic_feedback.dart`; covered by `test/ui/magic_feedback_test.dart`. +- **`TitleManager` treats a blank title suffix as absent.** A `null` suffix was already skipped, but an empty or whitespace suffix (e.g. an unset `APP_NAME` resolving to `""`) still produced `"Route | "` with a trailing separator and an empty tail. A blank suffix is now treated as absent (via `_withSuffix`), so the browser tab shows just the route title. Touches `lib/src/routing/title_manager.dart`; covered by `test/routing/title_manager_test.dart`. ## [0.0.4] - 2026-07-08 diff --git a/lib/src/ui/magic_feedback.dart b/lib/src/ui/magic_feedback.dart index dda97c6..24a9260 100644 --- a/lib/src/ui/magic_feedback.dart +++ b/lib/src/ui/magic_feedback.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:fluttersdk_wind/fluttersdk_wind.dart'; +import '../foundation/magic.dart'; import '../routing/magic_router.dart'; import '../facades/config.dart'; import '../facades/log.dart'; @@ -51,11 +52,9 @@ class MagicFeedback { String message, { String type = 'info', // Added Duration? duration, // Modified - Color? backgroundColor, - Color? color, }) { if (_context == null) { - Log.warning('MagicFeedback: Cannot show snackbar - context not mounted'); + _warn('MagicFeedback: Cannot show snackbar - context not mounted'); return; } @@ -119,7 +118,7 @@ class MagicFeedback { bool barrierDismissible = true, }) { if (_context == null) { - Log.warning('MagicFeedback: Cannot show dialog - context not mounted'); + _warn('MagicFeedback: Cannot show dialog - context not mounted'); return Future.value(null); } @@ -263,7 +262,7 @@ class MagicFeedback { /// Show a loading dialog. static void showLoading({String? message}) { if (_context == null) { - Log.warning('MagicFeedback: Cannot show loading - context not mounted'); + _warn('MagicFeedback: Cannot show loading - context not mounted'); return; } @@ -397,7 +396,7 @@ class MagicFeedback { final OverlayState? overlay = MagicRouter.instance.navigatorKey.currentState?.overlay; if (overlay == null) { - Log.warning('MagicFeedback: no Overlay available - toast skipped'); + _warn('MagicFeedback: no Overlay available - toast skipped'); return; } @@ -422,7 +421,16 @@ class MagicFeedback { duration: const Duration(milliseconds: 180), builder: (_, value, child) => Opacity(opacity: value, child: child), - child: content, + // A transparent Material gives the overlay subtree a real + // DefaultTextStyle. Without it the toast text inherits the + // root fallback style (the yellow double-underline debug + // decoration), since the Navigator's Overlay sits above the + // route's Material. Matches the snackbar/dialog/loading + // builders, which already wrap their content in a Material. + child: Material( + type: MaterialType.transparency, + child: content, + ), ), ), ), @@ -458,6 +466,23 @@ class MagicFeedback { } } + /// Logs a degradation warning through [Log] when the `log` service is bound, + /// falling back to [debugPrint] otherwise. + /// + /// Feedback can be triggered before `Magic.init` binds the logging service + /// (early startup, or a test that never registers it) or during teardown; + /// calling [Log.warning] then throws a "Service [log] is not registered" + /// error into the caller. Guarding on [Magic.bound] keeps the degrade path + /// from turning a missing snackbar into a crash, without silently dropping + /// the diagnostic. + static void _warn(String message) { + if (Magic.bound('log')) { + Log.warning(message); + } else { + debugPrint(message); + } + } + /// Parse Wind color class to Flutter Color. static Color _parseWindColor(String className) { if (className.contains('blue')) return const Color(0xFF3B82F6); diff --git a/test/ui/magic_feedback_test.dart b/test/ui/magic_feedback_test.dart index 9061336..aaaa8ec 100644 --- a/test/ui/magic_feedback_test.dart +++ b/test/ui/magic_feedback_test.dart @@ -57,4 +57,15 @@ void main() { await tester.pumpAndSettle(); expect(tester.takeException(), isNull); }); + + test('degrades without throwing when no context and the log service is ' + 'unbound', () { + // No harness pumped (so no navigator context) and `log` is unbound (setUp + // only flushes). The degrade path must fall back to debugPrint, not throw + // "Service [log] is not registered" into the caller. + expect( + () => MagicFeedback.error('No context', 'should not throw'), + returnsNormally, + ); + }); }