diff --git a/userfront/lib/features/auth/presentation/error_screen.dart b/userfront/lib/features/auth/presentation/error_screen.dart index b2ebc876..7977c0b6 100644 --- a/userfront/lib/features/auth/presentation/error_screen.dart +++ b/userfront/lib/features/auth/presentation/error_screen.dart @@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart'; import '../../../core/constants/error_whitelist.dart'; import '../../../core/i18n/locale_utils.dart'; import '../../../core/services/auth_proxy_service.dart'; +import '../../../core/widgets/theme_toggle_button.dart'; import 'package:userfront/i18n.dart'; class ErrorScreen extends StatelessWidget { @@ -22,6 +23,7 @@ class ErrorScreen extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final colorScheme = theme.colorScheme; final isProd = isProdOverride ?? AuthProxyService.isProdEnv; final normalizedCode = (errorCode ?? '').trim(); final hasCode = normalizedCode.isNotEmpty; @@ -62,7 +64,7 @@ class ErrorScreen extends StatelessWidget { : tr('msg.userfront.error.detail_request'))); return Scaffold( - backgroundColor: const Color(0xFFF7F8FA), + backgroundColor: colorScheme.surfaceContainerLowest, body: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 560), @@ -71,7 +73,7 @@ class ErrorScreen extends StatelessWidget { elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), - side: const BorderSide(color: Color(0xFFE5E7EB)), + side: BorderSide(color: colorScheme.outlineVariant), ), child: Padding( padding: const EdgeInsets.fromLTRB(28, 28, 28, 24), @@ -79,18 +81,25 @@ class ErrorScreen extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: theme.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w700, - color: const Color(0xFF111827), - ), + Row( + children: [ + Expanded( + child: Text( + title, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + color: colorScheme.onSurface, + ), + ), + ), + const ThemeToggleButton(compact: true), + ], ), const SizedBox(height: 12), Text( detail, style: theme.textTheme.bodyMedium?.copyWith( - color: const Color(0xFF4B5563), + color: colorScheme.onSurfaceVariant, height: 1.5, ), ), @@ -98,7 +107,7 @@ class ErrorScreen extends StatelessWidget { Text( tr('msg.userfront.error.type', params: {'type': errorType}), style: theme.textTheme.bodySmall?.copyWith( - color: const Color(0xFF6B7280), + color: colorScheme.onSurfaceVariant, ), ), if (errorId != null && errorId!.isNotEmpty) ...[ @@ -106,7 +115,7 @@ class ErrorScreen extends StatelessWidget { Text( tr('msg.userfront.error.id', params: {'id': errorId!}), style: theme.textTheme.bodySmall?.copyWith( - color: const Color(0xFF6B7280), + color: colorScheme.onSurfaceVariant, ), ), ], @@ -118,8 +127,8 @@ class ErrorScreen extends StatelessWidget { ElevatedButton( onPressed: () => context.go('/login'), style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF111827), - foregroundColor: Colors.white, + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, @@ -134,12 +143,12 @@ class ErrorScreen extends StatelessWidget { onPressed: () => context.go(buildLocalizedHomePath(Uri.base)), style: OutlinedButton.styleFrom( - foregroundColor: const Color(0xFF111827), + foregroundColor: colorScheme.onSurface, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), - side: const BorderSide(color: Color(0xFFCBD5F5)), + side: BorderSide(color: colorScheme.outline), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), diff --git a/userfront/lib/features/auth/presentation/login_screen.dart b/userfront/lib/features/auth/presentation/login_screen.dart index 460e56db..8e02a8a8 100644 --- a/userfront/lib/features/auth/presentation/login_screen.dart +++ b/userfront/lib/features/auth/presentation/login_screen.dart @@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:userfront/i18n.dart'; import '../../../core/widgets/language_selector.dart'; +import '../../../core/widgets/theme_toggle_button.dart'; import '../../../core/services/web_auth_integration.dart'; import '../../../core/services/auth_proxy_service.dart'; import '../../../core/services/auth_token_store.dart'; @@ -1385,6 +1386,10 @@ class _LoginScreenState extends ConsumerState @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final mutedColor = colorScheme.onSurfaceVariant; + if (_verificationOnly && _verificationApproved) { return Scaffold( appBar: AppBar( @@ -1393,12 +1398,14 @@ class _LoginScreenState extends ConsumerState icon: const Icon(Icons.arrow_back), onPressed: () => context.go(buildLocalizedHomePath(Uri.base)), ), + actions: const [ThemeToggleButton(compact: true)], ), body: _buildVerificationResultView(), ); } return Scaffold( + backgroundColor: colorScheme.surfaceContainerLowest, body: LayoutBuilder( builder: (context, constraints) { return SingleChildScrollView( @@ -1408,543 +1415,571 @@ class _LoginScreenState extends ConsumerState child: Container( constraints: const BoxConstraints(maxWidth: 400), padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - tr('ui.userfront.app_title'), - style: const TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - if (_drySendEnabled) ...[ - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, + child: Card( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + tr('ui.userfront.app_title'), + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, ), - decoration: BoxDecoration( - color: const Color(0xFFFFF3CD), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: const Color(0xFFFFC107)), - ), - child: Row( - children: [ - const Icon( - Icons.warning_amber_rounded, - color: Color(0xFF8A6D3B), + if (_drySendEnabled) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, ), - const SizedBox(width: 8), - Expanded( - child: Text( - tr('msg.userfront.login.dry_send'), - style: const TextStyle( - color: Color(0xFF8A6D3B), - fontSize: 12, - ), + decoration: BoxDecoration( + color: const Color(0xFFFFF3CD), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0xFFFFC107), ), ), - ], - ), - ), - ], - const SizedBox(height: 40), - - TabBar( - controller: _tabController, - tabs: [ - Tab(text: tr('ui.userfront.login.tabs.password')), - Tab(text: tr('ui.userfront.login.tabs.link')), - Tab(text: tr('ui.userfront.login.tabs.qr')), - ], - ), - const SizedBox(height: 24), - - SizedBox( - height: 350, - child: TabBarView( - controller: _tabController, - children: [ - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Column( + child: Row( children: [ - TextField( - key: const ValueKey( - 'password_login_id_input', - ), - controller: _passwordLoginIdController, - decoration: InputDecoration( - labelText: - _loginIdLabel ?? - tr( - 'ui.userfront.login.field.login_id', - ), - border: const OutlineInputBorder(), - prefixIcon: const Icon( - Icons.person_outline, - ), - ), - onSubmitted: (_) => _handlePasswordLogin(), + const Icon( + Icons.warning_amber_rounded, + color: Color(0xFF8A6D3B), ), - const SizedBox(height: 16), - TextField( - key: const ValueKey( - 'password_login_password_input', - ), - focusNode: _passwordFocusNode, - controller: _passwordController, - obscureText: true, - decoration: InputDecoration( - labelText: tr( - 'ui.userfront.login.field.password', - ), - border: const OutlineInputBorder(), - prefixIcon: const Icon( - Icons.lock_outline, - ), - ), - onSubmitted: (_) => _handlePasswordLogin(), - ), - if (_isPasswordCapsLockOn) ...[ - const SizedBox(height: 8), - Row( - children: [ - const Icon( - Icons.keyboard_capslock_rounded, - size: 18, - color: Colors.orange, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - _capsLockWarningText(context), - style: const TextStyle( - color: Colors.orange, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ], - const SizedBox(height: 24), - FilledButton( - key: const ValueKey( - 'password_login_submit_button', - ), - onPressed: _handlePasswordLogin, - style: FilledButton.styleFrom( - minimumSize: const Size.fromHeight(50), - ), + const SizedBox(width: 8), + Expanded( child: Text( - tr('ui.userfront.login.action.submit'), + tr('msg.userfront.login.dry_send'), + style: const TextStyle( + color: Color(0xFF8A6D3B), + fontSize: 12, + ), ), ), ], ), ), + ], + const SizedBox(height: 40), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Column( - children: [ - if (_linkPendingRef == null) ...[ - TextField( - controller: _linkIdController, - decoration: InputDecoration( - labelText: tr( - 'ui.userfront.login.field.login_id', + TabBar( + controller: _tabController, + tabs: [ + Tab(text: tr('ui.userfront.login.tabs.password')), + Tab(text: tr('ui.userfront.login.tabs.link')), + Tab(text: tr('ui.userfront.login.tabs.qr')), + ], + ), + const SizedBox(height: 24), + + SizedBox( + height: 350, + child: TabBarView( + controller: _tabController, + children: [ + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Column( + children: [ + TextField( + key: const ValueKey( + 'password_login_id_input', ), - hintText: '', - border: const OutlineInputBorder(), - prefixIcon: const Icon( - Icons.person_outline, + controller: _passwordLoginIdController, + decoration: InputDecoration( + labelText: + _loginIdLabel ?? + tr( + 'ui.userfront.login.field.login_id', + ), + prefixIcon: const Icon( + Icons.person_outline, + ), ), + onSubmitted: (_) => + _handlePasswordLogin(), ), - onSubmitted: (_) => _handleLinkLogin(), - ), - const SizedBox(height: 24), - FilledButton( - onPressed: _handleLinkLogin, - style: FilledButton.styleFrom( - minimumSize: const Size.fromHeight(50), - ), - child: Text( - tr('ui.userfront.login.link.send'), - ), - ), - const SizedBox(height: 24), - Text( - tr('msg.userfront.login.link.helper'), - style: const TextStyle( - color: Colors.grey, - fontSize: 12, - ), - textAlign: TextAlign.center, - ), - ], - if (_linkPendingRef != null) ...[ - if (_linkExpired) ...[ - Text( - tr('msg.userfront.login.link_timeout'), - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.grey, - fontSize: 12, + const SizedBox(height: 16), + TextField( + key: const ValueKey( + 'password_login_password_input', ), + focusNode: _passwordFocusNode, + controller: _passwordController, + obscureText: true, + decoration: InputDecoration( + labelText: tr( + 'ui.userfront.login.field.password', + ), + prefixIcon: const Icon( + Icons.lock_outline, + ), + ), + onSubmitted: (_) => + _handlePasswordLogin(), ), - const SizedBox(height: 12), + if (_isPasswordCapsLockOn) ...[ + const SizedBox(height: 8), + Row( + children: [ + const Icon( + Icons.keyboard_capslock_rounded, + size: 18, + color: Colors.orange, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + _capsLockWarningText(context), + style: const TextStyle( + color: Colors.orange, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ], + const SizedBox(height: 24), FilledButton( - onPressed: () { - setState(_resetLinkLoginState); - }, + key: const ValueKey( + 'password_login_submit_button', + ), + onPressed: _handlePasswordLogin, style: FilledButton.styleFrom( minimumSize: const Size.fromHeight( - 45, + 50, ), ), - child: Text(tr('ui.common.refresh')), - ), - ] else ...[ - Text( - tr( - 'msg.userfront.login.link.short_code_help', + child: Text( + tr( + 'ui.userfront.login.action.submit', + ), ), - style: const TextStyle( - color: Colors.grey, - fontSize: 12, - ), - textAlign: TextAlign.center, ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - flex: 2, - child: TextField( - controller: - _shortCodePrefixController, - textCapitalization: - TextCapitalization.characters, - decoration: InputDecoration( - labelText: tr( - 'ui.userfront.login.short_code.prefix', - ), - border: - const OutlineInputBorder(), - hintText: 'AB', - hintStyle: const TextStyle( - color: Colors.grey, - ), - ), - maxLength: 2, + ], + ), + ), + + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Column( + children: [ + if (_linkPendingRef == null) ...[ + TextField( + controller: _linkIdController, + decoration: InputDecoration( + labelText: tr( + 'ui.userfront.login.field.login_id', + ), + hintText: '', + prefixIcon: const Icon( + Icons.person_outline, ), ), - const SizedBox(width: 8), - Expanded( - flex: 4, - child: TextField( - controller: - _shortCodeDigitsController, - keyboardType: - TextInputType.number, - decoration: InputDecoration( - labelText: tr( - 'ui.userfront.login.short_code.digits', + onSubmitted: (_) => + _handleLinkLogin(), + ), + const SizedBox(height: 24), + FilledButton( + onPressed: _handleLinkLogin, + style: FilledButton.styleFrom( + minimumSize: const Size.fromHeight( + 50, + ), + ), + child: Text( + tr('ui.userfront.login.link.send'), + ), + ), + const SizedBox(height: 24), + Text( + tr('msg.userfront.login.link.helper'), + style: TextStyle( + color: mutedColor, + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + ], + if (_linkPendingRef != null) ...[ + if (_linkExpired) ...[ + Text( + tr( + 'msg.userfront.login.link_timeout', + ), + textAlign: TextAlign.center, + style: TextStyle( + color: mutedColor, + fontSize: 12, + ), + ), + const SizedBox(height: 12), + FilledButton( + onPressed: () { + setState(_resetLinkLoginState); + }, + style: FilledButton.styleFrom( + minimumSize: + const Size.fromHeight(45), + ), + child: Text( + tr('ui.common.refresh'), + ), + ), + ] else ...[ + Text( + tr( + 'msg.userfront.login.link.short_code_help', + ), + style: TextStyle( + color: mutedColor, + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + flex: 2, + child: TextField( + controller: + _shortCodePrefixController, + textCapitalization: + TextCapitalization + .characters, + decoration: InputDecoration( + labelText: tr( + 'ui.userfront.login.short_code.prefix', + ), + hintText: 'AB', + hintStyle: TextStyle( + color: mutedColor, + ), + ), + maxLength: 2, ), - border: - const OutlineInputBorder(), - hintText: '345678', - hintStyle: const TextStyle( - color: Colors.grey, - ), - suffixText: - _linkExpireSeconds > 0 - ? tr( - 'ui.userfront.login.short_code.expire_time', - params: { - 'time': _formatTime( - _linkExpireSeconds, - ), - }, - ) - : null, ), - maxLength: 6, + const SizedBox(width: 8), + Expanded( + flex: 4, + child: TextField( + controller: + _shortCodeDigitsController, + keyboardType: + TextInputType.number, + decoration: InputDecoration( + labelText: tr( + 'ui.userfront.login.short_code.digits', + ), + hintText: '345678', + hintStyle: TextStyle( + color: mutedColor, + ), + suffixText: + _linkExpireSeconds > 0 + ? tr( + 'ui.userfront.login.short_code.expire_time', + params: { + 'time': _formatTime( + _linkExpireSeconds, + ), + }, + ) + : null, + ), + maxLength: 6, + ), + ), + ], + ), + const SizedBox(height: 12), + FilledButton( + onPressed: () { + final prefix = + _shortCodePrefixController + .text + .trim() + .toUpperCase(); + final digits = + _shortCodeDigitsController + .text + .trim(); + if (prefix.length != 2 || + digits.length != 6) { + _showError( + tr( + 'msg.userfront.login.short_code.invalid', + ), + ); + return; + } + _verifyShortCode(prefix + digits); + }, + style: FilledButton.styleFrom( + minimumSize: + const Size.fromHeight(45), + ), + child: Text( + tr( + 'ui.userfront.login.short_code.submit', + ), + ), + ), + const SizedBox(height: 12), + TextButton( + onPressed: () { + if (_linkResendSeconds > 0) { + _showInfo( + tr( + 'msg.userfront.login.link.resend_wait', + params: { + 'time': _formatTime( + _linkResendSeconds, + ), + }, + ), + ); + return; + } + final loginId = + _lastLinkLoginId ?? + _linkIdController.text.trim(); + if (loginId.isEmpty) { + _showError( + tr( + 'msg.userfront.login.link.missing_login_id', + ), + ); + return; + } + _startEnchantedFlow( + loginId, + isEmail: + _lastLinkIsEmail || + loginId.contains('@'), + codeOnly: false, + ); + }, + child: Text( + _linkResendSeconds > 0 + ? tr( + 'ui.userfront.login.link.resend_with_time', + params: { + 'time': _formatTime( + _linkResendSeconds, + ), + }, + ) + : tr('ui.common.resend'), + ), + ), + if (!_lastLinkIsEmail) ...[ + const SizedBox(height: 4), + TextButton( + onPressed: () { + if (_linkResendSeconds > 0) { + _showInfo( + tr( + 'msg.userfront.login.link.resend_wait', + params: { + 'time': _formatTime( + _linkResendSeconds, + ), + }, + ), + ); + return; + } + final loginId = + _lastLinkLoginId ?? + _linkIdController.text + .trim(); + if (loginId.isEmpty) { + _showError( + tr( + 'msg.userfront.login.link.missing_phone', + ), + ); + return; + } + _startEnchantedFlow( + loginId, + isEmail: false, + codeOnly: true, + ); + }, + child: Text( + tr( + 'ui.userfront.login.link.code_only', + params: { + 'time': _formatTime( + _linkResendSeconds, + ), + }, + ), + ), + ), + ], + ], + ], + ], + ), + ), + + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (_isQrLoading) + const CircularProgressIndicator() + else if (_qrExpired) + Column( + children: [ + Text( + tr( + 'msg.userfront.login.qr_expired', + ), + textAlign: TextAlign.center, + style: TextStyle( + color: mutedColor, + fontSize: 12, + ), + ), + const SizedBox(height: 12), + FilledButton( + onPressed: _startQrFlow, + style: FilledButton.styleFrom( + minimumSize: + const Size.fromHeight(45), + ), + child: Text( + tr('ui.common.refresh'), ), ), ], - ), - const SizedBox(height: 12), - FilledButton( - onPressed: () { - final prefix = - _shortCodePrefixController.text - .trim() - .toUpperCase(); - final digits = - _shortCodeDigitsController.text - .trim(); - if (prefix.length != 2 || - digits.length != 6) { - _showError( - tr( - 'msg.userfront.login.short_code.invalid', + ) + else if (_qrImageBase64 != null) + Column( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border.all( + color: colorScheme.outline, ), - ); - return; - } - _verifyShortCode(prefix + digits); - }, - style: FilledButton.styleFrom( - minimumSize: const Size.fromHeight( - 45, - ), - ), - child: Text( - tr( - 'ui.userfront.login.short_code.submit', - ), - ), - ), - const SizedBox(height: 12), - TextButton( - onPressed: () { - if (_linkResendSeconds > 0) { - _showInfo( - tr( - 'msg.userfront.login.link.resend_wait', - params: { - 'time': _formatTime( - _linkResendSeconds, - ), - }, - ), - ); - return; - } - final loginId = - _lastLinkLoginId ?? - _linkIdController.text.trim(); - if (loginId.isEmpty) { - _showError( - tr( - 'msg.userfront.login.link.missing_login_id', - ), - ); - return; - } - _startEnchantedFlow( - loginId, - isEmail: - _lastLinkIsEmail || - loginId.contains('@'), - codeOnly: false, - ); - }, - child: Text( - _linkResendSeconds > 0 - ? tr( - 'ui.userfront.login.link.resend_with_time', - params: { - 'time': _formatTime( - _linkResendSeconds, - ), - }, - ) - : tr('ui.common.resend'), - ), - ), - if (!_lastLinkIsEmail) ...[ - const SizedBox(height: 4), - TextButton( - onPressed: () { - if (_linkResendSeconds > 0) { - _showInfo( - tr( - 'msg.userfront.login.link.resend_wait', - params: { - 'time': _formatTime( - _linkResendSeconds, - ), - }, - ), - ); - return; - } - final loginId = - _lastLinkLoginId ?? - _linkIdController.text.trim(); - if (loginId.isEmpty) { - _showError( - tr( - 'msg.userfront.login.link.missing_phone', - ), - ); - return; - } - _startEnchantedFlow( - loginId, - isEmail: false, - codeOnly: true, - ); - }, - child: Text( - tr( - 'ui.userfront.login.link.code_only', - params: { - 'time': _formatTime( - _linkResendSeconds, - ), - }, + borderRadius: + BorderRadius.circular(12), + ), + child: QrImageView( + data: _qrImageBase64!, + version: QrVersions.auto, + size: 200.0, + backgroundColor: Colors.white, ), ), - ), - ], - ], - ], - ], - ), - ), - - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (_isQrLoading) - const CircularProgressIndicator() - else if (_qrExpired) - Column( - children: [ - Text( - tr('msg.userfront.login.qr_expired'), - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.grey, - fontSize: 12, - ), - ), - const SizedBox(height: 12), - FilledButton( - onPressed: _startQrFlow, - style: FilledButton.styleFrom( - minimumSize: const Size.fromHeight( - 45, - ), - ), - child: Text(tr('ui.common.refresh')), - ), - ], - ) - else if (_qrImageBase64 != null) - Column( - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border.all( - color: Colors.grey.shade300, - ), - borderRadius: BorderRadius.circular( - 12, - ), - ), - child: QrImageView( - data: _qrImageBase64!, - version: QrVersions.auto, - size: 200.0, - ), - ), - const SizedBox(height: 12), - Text( - _qrRemainingSeconds > 0 - ? tr( - 'ui.userfront.login.qr.remaining', - params: { - 'time': _formatTime( - _qrRemainingSeconds, + const SizedBox(height: 12), + Text( + _qrRemainingSeconds > 0 + ? tr( + 'ui.userfront.login.qr.remaining', + params: { + 'time': _formatTime( + _qrRemainingSeconds, + ), + }, + ) + : tr( + 'ui.userfront.login.qr.expired', ), - }, - ) - : tr( - 'ui.userfront.login.qr.expired', + textAlign: TextAlign.center, + style: TextStyle( + color: _qrRemainingSeconds > 30 + ? Colors.blue + : Colors.red, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + tr( + 'msg.userfront.login.qr.scan_hint', + ), + textAlign: TextAlign.center, + style: TextStyle( + color: mutedColor, + fontSize: 12, + ), + ), + TextButton( + onPressed: _startQrFlow, + child: Text( + tr( + 'ui.userfront.login.qr.refresh', ), - textAlign: TextAlign.center, - style: TextStyle( - color: _qrRemainingSeconds > 30 - ? Colors.blue - : Colors.red, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), + ), + ), + ], + ) + else Text( - tr('msg.userfront.login.qr.scan_hint'), + tr( + 'msg.userfront.login.qr.load_failed', + ), textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.grey, - fontSize: 12, - ), ), - TextButton( - onPressed: _startQrFlow, - child: Text( - tr('ui.userfront.login.qr.refresh'), - ), - ), - ], - ) - else - Text( - tr('msg.userfront.login.qr.load_failed'), - textAlign: TextAlign.center, - ), + ], + ), ], ), - ], - ), - ), - const SizedBox(height: 16), - Column( - children: [ - TextButton( - onPressed: () => context.push('/forgot-password'), - child: Text( - tr('ui.userfront.login.forgot_password'), - ), ), - Row( - mainAxisAlignment: MainAxisAlignment.center, + const SizedBox(height: 16), + Column( children: [ - Text( - tr('msg.userfront.login.no_account'), - style: const TextStyle( - color: Colors.grey, - fontSize: 14, + TextButton( + onPressed: () => + context.push('/forgot-password'), + child: Text( + tr('ui.userfront.login.forgot_password'), ), ), - TextButton( - onPressed: () => context.push('/signup'), - child: Text(tr('ui.userfront.login.signup')), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + tr('msg.userfront.login.no_account'), + style: TextStyle( + color: mutedColor, + fontSize: 14, + ), + ), + TextButton( + onPressed: () => context.push('/signup'), + child: Text( + tr('ui.userfront.login.signup'), + ), + ), + ], ), ], ), + const SizedBox(height: 6), + const Wrap( + alignment: WrapAlignment.center, + spacing: 10, + runSpacing: 10, + children: [ThemeToggleButton(), LanguageSelector()], + ), ], ), - const SizedBox(height: 6), - const Align( - alignment: Alignment.center, - child: LanguageSelector(), - ), - ], + ), ), ), ), diff --git a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart index 5d46255c..4dea1f97 100644 --- a/userfront/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/userfront/lib/features/dashboard/presentation/dashboard_screen.dart @@ -18,6 +18,7 @@ import '../../../../core/services/auth_token_store.dart'; import '../../../../core/services/http_client.dart'; import '../../../../core/i18n/locale_utils.dart'; import '../../../../core/widgets/language_selector.dart'; +import '../../../../core/widgets/theme_toggle_button.dart'; import '../../../../core/ui/layout_breakpoints.dart'; import '../../../../core/ui/toast_service.dart'; import '../../profile/domain/notifiers/profile_notifier.dart'; @@ -33,10 +34,6 @@ class DashboardScreen extends ConsumerStatefulWidget { } class _DashboardScreenState extends ConsumerState { - static const _ink = Color(0xFF1A1F2C); - static const _surface = Colors.white; - static const _border = Color(0xFFE5E7EB); - static const _subtle = Color(0xFFF7F8FA); static const double _dashboardCardSpacing = 12; static const double _dashboardCardMaxWidth = 228; static const double _activityDialogMaxWidth = 360; @@ -66,8 +63,14 @@ class _DashboardScreenState extends ConsumerState { bool _showAllActivities = false; bool _showActiveSessionsOnly = false; + bool _isDesktopSideMenuOpen = true; final Set _revokedClientIds = {}; + Color get _ink => Theme.of(context).colorScheme.onSurface; + Color get _surface => Theme.of(context).colorScheme.surface; + Color get _border => Theme.of(context).colorScheme.outlineVariant; + Color get _subtle => Theme.of(context).colorScheme.surfaceContainerLowest; + String _renderTranslatedText( String key, { String? fallback, @@ -275,7 +278,7 @@ class _DashboardScreenState extends ConsumerState { children: [ Text( item.appName, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: _ink, @@ -346,7 +349,7 @@ class _DashboardScreenState extends ConsumerState { child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon( + Icon( Icons.shield_outlined, size: 14, color: _ink, @@ -354,7 +357,7 @@ class _DashboardScreenState extends ConsumerState { const SizedBox(width: 6), Text( scope, - style: const TextStyle( + style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: _ink, @@ -414,7 +417,7 @@ class _DashboardScreenState extends ConsumerState { children: [ Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: _ink, @@ -519,7 +522,14 @@ class _DashboardScreenState extends ConsumerState { ), const Padding( padding: EdgeInsets.only(bottom: 16), - child: LanguageSelector(compact: true), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ThemeToggleButton(), + SizedBox(height: 8), + LanguageSelector(compact: true), + ], + ), ), ], ), @@ -942,14 +952,35 @@ class _DashboardScreenState extends ConsumerState { return Scaffold( backgroundColor: _subtle, appBar: AppBar( + leading: isWide + ? IconButton( + icon: Icon( + _isDesktopSideMenuOpen ? Icons.menu_open : Icons.menu, + ), + tooltip: _isDesktopSideMenuOpen + ? tr('ui.common.collapse') + : '펼치기', + onPressed: () { + setState(() { + _isDesktopSideMenuOpen = !_isDesktopSideMenuOpen; + }); + }, + ) + : Builder( + builder: (context) => IconButton( + icon: const Icon(Icons.menu), + tooltip: MaterialLocalizations.of( + context, + ).openAppDrawerTooltip, + onPressed: () => Scaffold.of(context).openDrawer(), + ), + ), title: Text( tr('ui.userfront.app_title'), style: const TextStyle(fontWeight: FontWeight.bold), ), - elevation: 0, - backgroundColor: _surface, - foregroundColor: Colors.black, actions: [ + const ThemeToggleButton(compact: true), IconButton( icon: const Icon(Icons.person_outline), tooltip: tr('ui.userfront.nav.profile'), @@ -972,7 +1003,7 @@ class _DashboardScreenState extends ConsumerState { : Drawer(child: _buildSideMenu(context, closeOnTap: true)), body: Row( children: [ - if (isWide) + if (isWide && _isDesktopSideMenuOpen) SizedBox( width: 240, child: _buildSideMenu(context, closeOnTap: false), @@ -1065,7 +1096,7 @@ class _DashboardScreenState extends ConsumerState { fallback: 'Hello, {{name}}.', values: {'name': userName}, ), - style: const TextStyle( + style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: _ink, @@ -1117,7 +1148,7 @@ class _DashboardScreenState extends ConsumerState { children: [ Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: _ink, @@ -1271,7 +1302,7 @@ class _DashboardScreenState extends ConsumerState { const SizedBox(width: 6), Text( label, - style: const TextStyle( + style: TextStyle( fontSize: 12, color: _ink, fontWeight: FontWeight.w600, @@ -1496,7 +1527,7 @@ class _DashboardScreenState extends ConsumerState { Expanded( child: Text( item.appName, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: _ink, @@ -1530,7 +1561,7 @@ class _DashboardScreenState extends ConsumerState { const SizedBox(height: 4), Text( item.lastAuthAt, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: _ink, @@ -1544,7 +1575,7 @@ class _DashboardScreenState extends ConsumerState { onPressed: () => _showRpDetails(item), style: OutlinedButton.styleFrom( foregroundColor: _ink, - side: const BorderSide(color: _border), + side: BorderSide(color: _border), padding: const EdgeInsets.symmetric(vertical: 7), ), child: Text( @@ -1745,7 +1776,7 @@ class _DashboardScreenState extends ConsumerState { children: [ Text( tr('ui.userfront.audit.filter.title'), - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, color: _ink, @@ -1765,7 +1796,7 @@ class _DashboardScreenState extends ConsumerState { children: [ Text( tr('ui.userfront.audit.filter.toggle_label'), - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: _ink, @@ -2224,7 +2255,7 @@ class _DashboardScreenState extends ConsumerState { Expanded( child: _buildAppCell( log, - style: const TextStyle( + style: TextStyle( fontWeight: FontWeight.w600, color: _ink, ), diff --git a/userfront/lib/features/profile/presentation/pages/profile_page.dart b/userfront/lib/features/profile/presentation/pages/profile_page.dart index 9d25748a..b1b2a04e 100644 --- a/userfront/lib/features/profile/presentation/pages/profile_page.dart +++ b/userfront/lib/features/profile/presentation/pages/profile_page.dart @@ -9,6 +9,7 @@ import '../../../../core/services/logout_service.dart'; import '../../../../core/ui/layout_breakpoints.dart'; import '../../../../core/ui/toast_service.dart'; import '../../../../core/widgets/language_selector.dart'; +import '../../../../core/widgets/theme_toggle_button.dart'; import '../../data/models/user_profile_model.dart'; import '../../domain/notifiers/profile_notifier.dart'; @@ -20,10 +21,6 @@ class ProfilePage extends ConsumerStatefulWidget { } class _ProfilePageState extends ConsumerState { - static const _ink = Color(0xFF1A1F2C); - static const _surface = Colors.white; - static const _border = Color(0xFFE5E7EB); - static const _subtle = Color(0xFFF7F8FA); static final _log = Logger('ProfilePage'); UserProfile? _cachedProfile; @@ -54,9 +51,15 @@ class _ProfilePageState extends ConsumerState { bool _showCurrentPassword = false; bool _showNewPassword = false; bool _showConfirmPassword = false; + bool _isDesktopSideMenuOpen = true; Map? _passwordPolicy; bool _isPasswordPolicyLoading = false; + Color get _ink => Theme.of(context).colorScheme.onSurface; + Color get _surface => Theme.of(context).colorScheme.surface; + Color get _border => Theme.of(context).colorScheme.outlineVariant; + Color get _subtle => Theme.of(context).colorScheme.surfaceContainerLowest; + String _renderTranslatedText( String key, { String? fallback, @@ -615,7 +618,14 @@ class _ProfilePageState extends ConsumerState { ), const Padding( padding: EdgeInsets.only(bottom: 16), - child: LanguageSelector(compact: true), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ThemeToggleButton(), + SizedBox(height: 8), + LanguageSelector(compact: true), + ], + ), ), ], ); @@ -627,7 +637,7 @@ class _ProfilePageState extends ConsumerState { children: [ Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: _ink, @@ -654,7 +664,7 @@ class _ProfilePageState extends ConsumerState { const SizedBox(width: 6), Text( label, - style: const TextStyle( + style: TextStyle( fontSize: 12, color: _ink, fontWeight: FontWeight.w600, @@ -705,7 +715,7 @@ class _ProfilePageState extends ConsumerState { fallback: 'Hello, {{name}}.', values: {'name': name}, ), - style: const TextStyle( + style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, color: _ink, @@ -996,12 +1006,17 @@ class _ProfilePageState extends ConsumerState { const SizedBox(height: 8), Text( tr('msg.userfront.profile.password.subtitle'), - style: const TextStyle(color: Color(0xFF6B7280)), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), const SizedBox(height: 8), Text( _buildPasswordPolicyDescription(), - style: const TextStyle(color: Color(0xFF6B7280), fontSize: 12), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12, + ), ), const SizedBox(height: 16), TextField( @@ -1231,14 +1246,35 @@ class _ProfilePageState extends ConsumerState { return Scaffold( backgroundColor: _subtle, appBar: AppBar( + leading: isWide + ? IconButton( + icon: Icon( + _isDesktopSideMenuOpen ? Icons.menu_open : Icons.menu, + ), + tooltip: _isDesktopSideMenuOpen + ? tr('ui.common.collapse') + : '펼치기', + onPressed: () { + setState(() { + _isDesktopSideMenuOpen = !_isDesktopSideMenuOpen; + }); + }, + ) + : Builder( + builder: (context) => IconButton( + icon: const Icon(Icons.menu), + tooltip: MaterialLocalizations.of( + context, + ).openAppDrawerTooltip, + onPressed: () => Scaffold.of(context).openDrawer(), + ), + ), title: Text( tr('ui.userfront.app_title'), style: const TextStyle(fontWeight: FontWeight.bold), ), - elevation: 0, - backgroundColor: _surface, - foregroundColor: Colors.black, actions: [ + const ThemeToggleButton(compact: true), IconButton( icon: const Icon(Icons.home_outlined), tooltip: tr('ui.userfront.nav.dashboard'), @@ -1259,7 +1295,8 @@ class _ProfilePageState extends ConsumerState { drawer: isWide ? null : Drawer(child: _buildSideMenu(context)), body: Row( children: [ - if (isWide) SizedBox(width: 240, child: _buildSideMenu(context)), + if (isWide && _isDesktopSideMenuOpen) + SizedBox(width: 240, child: _buildSideMenu(context)), Expanded(child: _buildContent(profile, isUpdating)), ], ),