diff --git a/lib/app/app_shell.dart b/lib/app/app_shell.dart index 1df58450..a9ad95f9 100644 --- a/lib/app/app_shell.dart +++ b/lib/app/app_shell.dart @@ -57,6 +57,7 @@ class _AppShellState extends State { final controller = widget.controller; return Scaffold( body: SafeArea( + bottom: false, child: LayoutBuilder( builder: (context, constraints) { final palette = context.palette; diff --git a/lib/features/account/account_page.dart b/lib/features/account/account_page.dart index 4488b56a..eda89d0c 100644 --- a/lib/features/account/account_page.dart +++ b/lib/features/account/account_page.dart @@ -28,7 +28,7 @@ class _AccountPageState extends State { animation: controller, builder: (context, _) { return SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(32, 32, 32, 16), + padding: const EdgeInsets.fromLTRB(32, 32, 32, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/features/assistant/assistant_page.dart b/lib/features/assistant/assistant_page.dart index 2a9f57f2..2252c014 100644 --- a/lib/features/assistant/assistant_page.dart +++ b/lib/features/assistant/assistant_page.dart @@ -10,6 +10,7 @@ import '../../i18n/app_language.dart'; import '../../models/app_models.dart'; import '../../runtime/runtime_models.dart'; import '../../theme/app_palette.dart'; +import '../../theme/app_theme.dart'; import '../../widgets/gateway_connect_dialog.dart'; import '../../widgets/pane_resize_handle.dart'; import '../../widgets/surface_card.dart'; @@ -37,7 +38,7 @@ class _AssistantPageState extends State { late final FocusNode _composerFocusNode; String _mode = 'ask'; String _thinkingLabel = 'high'; - double _conversationPaneRatio = 0.64; + double _conversationPaneRatio = 0.7; List<_ComposerAttachment> _attachments = const <_ComposerAttachment>[]; String? _lastSubmittedPrompt; String? _lastAutoAgentLabel; @@ -80,21 +81,21 @@ class _AssistantPageState extends State { }); return Padding( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + padding: const EdgeInsets.fromLTRB(6, 6, 6, 6), child: LayoutBuilder( builder: (context, constraints) { - const handleHeight = 12.0; - const paneGap = 8.0; + const handleHeight = 10.0; + const paneGap = 6.0; final availablePaneHeight = (constraints.maxHeight - handleHeight - paneGap) .clamp(0.0, double.infinity) .toDouble(); var minConversationHeight = availablePaneHeight >= 620 - ? 220.0 - : availablePaneHeight * 0.34; + ? 240.0 + : availablePaneHeight * 0.4; var minComposerHeight = availablePaneHeight >= 620 - ? 248.0 - : availablePaneHeight * 0.30; + ? 176.0 + : availablePaneHeight * 0.24; if (minConversationHeight + minComposerHeight > availablePaneHeight) { minConversationHeight = availablePaneHeight * 0.52; @@ -578,12 +579,12 @@ class _ConversationArea extends StatelessWidget { final theme = Theme.of(context); return SurfaceCard( - borderRadius: 14, + borderRadius: 12, padding: EdgeInsets.zero, child: Column( children: [ Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 10), + padding: const EdgeInsets.fromLTRB(14, 10, 14, 8), child: Row( children: [ Expanded( @@ -594,7 +595,7 @@ class _ConversationArea extends StatelessWidget { controller.currentSessionKey, style: theme.textTheme.titleLarge, ), - const SizedBox(height: 2), + const SizedBox(height: 4), Text( controller.connection.status == RuntimeConnectionStatus.connected @@ -628,10 +629,10 @@ class _ConversationArea extends StatelessWidget { ) : ListView.separated( controller: scrollController, - padding: const EdgeInsets.fromLTRB(18, 16, 18, 16), + padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), physics: const BouncingScrollPhysics(), itemCount: items.length, - separatorBuilder: (_, _) => const SizedBox(height: 10), + separatorBuilder: (_, _) => const SizedBox(height: 8), itemBuilder: (context, index) { final item = items[index]; return switch (item.kind) { @@ -796,22 +797,22 @@ class _AssistantEmptyState extends StatelessWidget { return Center( child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 520), + constraints: const BoxConstraints(maxWidth: 500), child: Padding( - padding: const EdgeInsets.all(24), + padding: const EdgeInsets.all(16), child: SurfaceCard( - borderRadius: 20, + borderRadius: 12, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: theme.textTheme.headlineSmall), - const SizedBox(height: 10), + const SizedBox(height: 8), Text(description, style: theme.textTheme.bodyMedium), - const SizedBox(height: 18), + const SizedBox(height: 12), Wrap( - spacing: 12, - runSpacing: 12, + spacing: 8, + runSpacing: 8, children: [ FilledButton.icon( onPressed: connected @@ -925,7 +926,7 @@ class _ComposerBar extends StatelessWidget { : appText('连接', 'Connect'); return SurfaceCard( - borderRadius: 16, + borderRadius: 12, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -936,21 +937,21 @@ class _ComposerBar extends StatelessWidget { children: attachments .map( (attachment) => InputChip( - avatar: Icon(attachment.icon, size: 18), + avatar: Icon(attachment.icon, size: 16), label: Text(attachment.name), onDeleted: () => onRemoveAttachment(attachment), ), ) .toList(), ), - const SizedBox(height: 10), + const SizedBox(height: 8), ], TextField( controller: inputController, focusNode: focusNode, autofocus: true, - minLines: 4, - maxLines: 8, + minLines: 2, + maxLines: 6, decoration: InputDecoration( border: InputBorder.none, isCollapsed: true, @@ -961,7 +962,7 @@ class _ComposerBar extends StatelessWidget { ), onSubmitted: (_) => onSend(), ), - const SizedBox(height: 12), + const SizedBox(height: 8), Row( children: [ Expanded( @@ -1225,12 +1226,12 @@ class _ComposerBar extends StatelessWidget { : onOpenGateway, style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 10, + horizontal: 12, + vertical: 8, ), - minimumSize: const Size(92, 40), + minimumSize: const Size(80, 34), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(999), + borderRadius: BorderRadius.circular(8), ), ), child: Row( @@ -1268,14 +1269,14 @@ class _ComposerIconButton extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - width: 36, - height: 36, + width: 32, + height: 32, decoration: BoxDecoration( color: context.palette.surfaceSecondary, - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(8), border: Border.all(color: context.palette.strokeSoft), ), - child: Icon(icon, size: 18, color: context.palette.textMuted), + child: Icon(icon, size: 16, color: context.palette.textMuted), ); } } @@ -1305,16 +1306,16 @@ class _ComposerToolbarChip extends StatelessWidget { final palette = context.palette; return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs, vertical: 6), decoration: BoxDecoration( color: backgroundColor ?? palette.surfaceSecondary, - borderRadius: BorderRadius.circular(999), + borderRadius: BorderRadius.circular(AppRadius.chip), border: Border.all(color: borderColor ?? palette.strokeSoft), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: 15, color: foregroundColor ?? palette.textMuted), + Icon(icon, size: 14, color: foregroundColor ?? palette.textMuted), const SizedBox(width: 6), ConstrainedBox( constraints: BoxConstraints(maxWidth: maxLabelWidth), @@ -1328,10 +1329,10 @@ class _ComposerToolbarChip extends StatelessWidget { ), ), if (showChevron) ...[ - const SizedBox(width: 4), + const SizedBox(width: 2), Icon( Icons.keyboard_arrow_down_rounded, - size: 16, + size: 14, color: foregroundColor ?? palette.textMuted, ), ], @@ -1369,29 +1370,22 @@ class _MessageBubble extends StatelessWidget { child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 760), child: Container( - padding: const EdgeInsets.all(14), + padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppRadius.card), border: Border.all(color: borderColor), - boxShadow: [ - BoxShadow( - color: palette.shadow.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, 5), - ), - ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: theme.textTheme.labelLarge), - const SizedBox(height: 6), + const SizedBox(height: 4), SelectableText( text.isEmpty ? appText('暂无内容。', 'No content yet.') : text, style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurface, - height: 1.55, + height: 1.45, ), ), ], @@ -1450,19 +1444,12 @@ class _TaskStatusCard extends StatelessWidget { constraints: const BoxConstraints(maxWidth: 760), child: Material( color: Colors.white, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppRadius.card), child: Container( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(AppSpacing.sm), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppRadius.card), border: Border.all(color: palette.strokeSoft), - boxShadow: [ - BoxShadow( - color: palette.shadow.withValues(alpha: 0.03), - blurRadius: 10, - offset: const Offset(0, 6), - ), - ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1471,15 +1458,15 @@ class _TaskStatusCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - width: 28, - height: 28, + width: 24, + height: 24, decoration: BoxDecoration( color: statusStyle.backgroundColor, - borderRadius: BorderRadius.circular(9), + borderRadius: BorderRadius.circular(8), ), child: Icon( icon, - size: 15, + size: 14, color: statusStyle.foregroundColor, ), ), @@ -1502,16 +1489,16 @@ class _TaskStatusCard extends StatelessWidget { ), ], ), - const SizedBox(height: 8), + const SizedBox(height: 6), Container( width: double.infinity, padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 8, + horizontal: 8, + vertical: 6, ), decoration: BoxDecoration( color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(10), ), child: Wrap( spacing: 10, @@ -1528,7 +1515,7 @@ class _TaskStatusCard extends StatelessWidget { ], ), ), - const SizedBox(height: 8), + const SizedBox(height: 6), Row( children: [ Text( @@ -1608,18 +1595,18 @@ class _ToolCallTileState extends State<_ToolCallTile> { child: Container( decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppRadius.card), border: Border.all(color: palette.strokeSoft), ), child: Column( children: [ InkWell( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppRadius.card), onTap: () => setState(() => _expanded = !_expanded), child: Padding( padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, + horizontal: 10, + vertical: 8, ), child: Row( children: [ @@ -1653,7 +1640,7 @@ class _ToolCallTileState extends State<_ToolCallTile> { ), ), ), - const SizedBox(width: 10), + const SizedBox(width: 8), _StatusPill( label: _toolCallStatusLabel(statusLabel), backgroundColor: statusStyle.backgroundColor, @@ -1677,12 +1664,12 @@ class _ToolCallTileState extends State<_ToolCallTile> { curve: Curves.easeOutCubic, child: _expanded ? Padding( - padding: const EdgeInsets.fromLTRB(12, 0, 12, 10), + padding: const EdgeInsets.fromLTRB(AppSpacing.sm, 0, AppSpacing.sm, AppSpacing.xs), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Divider(height: 1, color: palette.strokeSoft), - const SizedBox(height: 8), + const SizedBox(height: 6), Text( widget.summary.trim().isEmpty ? appText( @@ -1692,7 +1679,7 @@ class _ToolCallTileState extends State<_ToolCallTile> { : widget.summary.trim(), style: theme.textTheme.bodySmall, ), - const SizedBox(height: 6), + const SizedBox(height: 4), TextButton( onPressed: widget.onOpenDetail, child: Text(appText('打开详情', 'Open detail')), @@ -1725,12 +1712,12 @@ class _StatusPill extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: backgroundColor ?? Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(999), + borderRadius: BorderRadius.circular(AppRadius.badge), ), child: Text( label, @@ -1761,14 +1748,14 @@ class _ConnectionChip extends StatelessWidget { }; return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs, vertical: 5), decoration: BoxDecoration( color: color, - borderRadius: BorderRadius.circular(999), + borderRadius: BorderRadius.circular(AppRadius.chip), ), child: Text( '${connection.status.label} · ${connection.remoteAddress ?? appText('未连接目标', 'No target')}', - style: theme.textTheme.labelLarge, + style: theme.textTheme.labelMedium, ), ); } diff --git a/lib/features/modules/modules_page.dart b/lib/features/modules/modules_page.dart index 2536465c..d11a76da 100644 --- a/lib/features/modules/modules_page.dart +++ b/lib/features/modules/modules_page.dart @@ -62,7 +62,7 @@ class _ModulesPageState extends State { animation: controller, builder: (context, _) { return SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(32, 32, 32, 16), + padding: const EdgeInsets.fromLTRB(32, 32, 32, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/features/secrets/secrets_page.dart b/lib/features/secrets/secrets_page.dart index 89ccc817..ae16c6f2 100644 --- a/lib/features/secrets/secrets_page.dart +++ b/lib/features/secrets/secrets_page.dart @@ -35,7 +35,7 @@ class _SecretsPageState extends State { animation: controller, builder: (context, _) { return SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(32, 32, 32, 16), + padding: const EdgeInsets.fromLTRB(32, 32, 32, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/features/settings/settings_page.dart b/lib/features/settings/settings_page.dart index 5f58b3cd..7539b230 100644 --- a/lib/features/settings/settings_page.dart +++ b/lib/features/settings/settings_page.dart @@ -70,7 +70,7 @@ class _SettingsPageState extends State { builder: (context, _) { final settings = controller.settings; return SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(32, 32, 32, 16), + padding: const EdgeInsets.fromLTRB(32, 32, 32, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/features/tasks/tasks_page.dart b/lib/features/tasks/tasks_page.dart index e8445941..ca260020 100644 --- a/lib/features/tasks/tasks_page.dart +++ b/lib/features/tasks/tasks_page.dart @@ -67,7 +67,7 @@ class _TasksPageState extends State { animation: controller, builder: (context, _) { return SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(32, 32, 32, 16), + padding: const EdgeInsets.fromLTRB(32, 32, 32, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index c02ff532..0666fe9e 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -3,6 +3,79 @@ import 'package:flutter/material.dart'; import 'app_palette.dart'; +/// Design tokens for the XWorkmate design system. +/// Follows a modern AI developer tool design language with: +/// - 8px grid spacing +/// - Compact, neutral, professional aesthetic +/// - Consistent border radii +class AppSpacing { + AppSpacing._(); + + // 8px grid system + static const double xxs = 4.0; + static const double xs = 8.0; + static const double sm = 12.0; + static const double md = 16.0; + static const double lg = 24.0; + static const double xl = 32.0; +} + +class AppRadius { + AppRadius._(); + + static const double card = 12.0; + static const double button = 8.0; + static const double input = 10.0; + static const double chip = 12.0; + static const double badge = 10.0; + static const double dialog = 16.0; + static const double sidebar = 14.0; + static const double icon = 8.0; +} + +class AppTypography { + AppTypography._(); + + // H1 - 22px weight 600 + static const double h1Size = 22.0; + static const FontWeight h1Weight = FontWeight.w600; + static const double h1Height = 1.25; + + // H2 - 18px weight 600 + static const double h2Size = 18.0; + static const FontWeight h2Weight = FontWeight.w600; + static const double h2Height = 1.3; + + // Body - 14px weight 400 + static const double bodySize = 14.0; + static const FontWeight bodyWeight = FontWeight.w400; + static const double bodyHeight = 1.4; + + // Meta - 12px weight 400 + static const double metaSize = 12.0; + static const FontWeight metaWeight = FontWeight.w400; + static const double metaHeight = 1.45; +} + +class AppSizes { + AppSizes._(); + + // Sidebar + static const double sidebarItemHeight = 36.0; + static const double sidebarIconSize = 18.0; + static const double sidebarTextSize = 14.0; + static const double sidebarExpandedWidth = 204.0; + static const double sidebarCollapsedWidth = 72.0; + + // Input area + static const double textareaHeight = 48.0; + static const double toolbarHeight = 36.0; + + // Buttons + static const double buttonHeightDesktop = 34.0; + static const double buttonHeightMobile = 36.0; +} + class AppTheme { static ThemeData light() => _theme(brightness: Brightness.light, palette: AppPalette.light); @@ -83,7 +156,7 @@ class AppTheme { shadowColor: palette.shadow, surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(AppRadius.card), side: BorderSide(color: palette.strokeSoft), ), ), @@ -91,7 +164,7 @@ class AppTheme { backgroundColor: palette.surfaceSecondary, side: BorderSide(color: palette.strokeSoft), labelStyle: tunedTextTheme.labelMedium, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.chip)), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), ), filledButtonTheme: FilledButtonThemeData( @@ -99,13 +172,13 @@ class AppTheme { textStyle: tunedTextTheme.labelLarge?.copyWith( fontWeight: FontWeight.w500, ), - minimumSize: Size(0, isDesktop ? 34 : 36), + minimumSize: Size(0, isDesktop ? AppSizes.buttonHeightDesktop : AppSizes.buttonHeightMobile), padding: EdgeInsets.symmetric( - horizontal: isDesktop ? 12 : 14, - vertical: isDesktop ? 8 : 9, + horizontal: AppSpacing.sm, + vertical: AppSpacing.xs, ), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.button), ), ), ), @@ -115,13 +188,13 @@ class AppTheme { textStyle: tunedTextTheme.labelLarge?.copyWith( fontWeight: FontWeight.w500, ), - minimumSize: Size(0, isDesktop ? 34 : 36), + minimumSize: Size(0, isDesktop ? AppSizes.buttonHeightDesktop : AppSizes.buttonHeightMobile), padding: EdgeInsets.symmetric( - horizontal: isDesktop ? 12 : 14, - vertical: isDesktop ? 8 : 9, + horizontal: AppSpacing.sm, + vertical: AppSpacing.xs, ), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.button), ), side: BorderSide(color: palette.strokeSoft), ), @@ -134,11 +207,11 @@ class AppTheme { ), minimumSize: Size(0, isDesktop ? 32 : 34), padding: EdgeInsets.symmetric( - horizontal: isDesktop ? 10 : 12, - vertical: isDesktop ? 8 : 9, + horizontal: AppSpacing.sm, + vertical: AppSpacing.xs, ), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.button), ), ), ), @@ -147,7 +220,7 @@ class AppTheme { foregroundColor: palette.textSecondary, backgroundColor: palette.surfaceSecondary, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.icon), side: BorderSide(color: palette.strokeSoft), ), minimumSize: const Size(34, 34), @@ -164,19 +237,19 @@ class AppTheme { color: palette.textMuted, ), contentPadding: EdgeInsets.symmetric( - horizontal: isDesktop ? 16 : 18, - vertical: isDesktop ? 12 : 13, + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, ), border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.input), borderSide: BorderSide(color: palette.strokeSoft), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.input), borderSide: BorderSide(color: palette.strokeSoft), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.input), borderSide: BorderSide(color: palette.accent.withValues(alpha: 0.42)), ), ), @@ -196,10 +269,10 @@ class AppTheme { return palette.textSecondary; }), padding: const WidgetStatePropertyAll( - EdgeInsets.symmetric(horizontal: 12, vertical: 8), + EdgeInsets.symmetric(horizontal: AppSpacing.sm, vertical: AppSpacing.xs), ), shape: WidgetStatePropertyAll( - RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.chip)), ), textStyle: WidgetStatePropertyAll( tunedTextTheme.labelLarge?.copyWith(fontWeight: FontWeight.w500), @@ -210,7 +283,7 @@ class AppTheme { behavior: SnackBarBehavior.floating, backgroundColor: palette.surfaceTertiary, contentTextStyle: TextStyle(color: palette.textPrimary), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppRadius.dialog)), ), ); } @@ -238,28 +311,30 @@ class AppTheme { } return base.copyWith( + // H1: 22px weight 600 displaySmall: withUiFont( base.displaySmall?.copyWith( - fontSize: isDesktop ? 22 : 24, - fontWeight: FontWeight.w600, + fontSize: AppTypography.h1Size, + fontWeight: AppTypography.h1Weight, letterSpacing: -0.24, - height: 1.25, + height: AppTypography.h1Height, ), ), headlineSmall: withUiFont( base.headlineSmall?.copyWith( - fontSize: isDesktop ? 22 : 24, - fontWeight: FontWeight.w600, + fontSize: AppTypography.h1Size, + fontWeight: AppTypography.h1Weight, letterSpacing: -0.24, - height: 1.25, + height: AppTypography.h1Height, ), ), + // H2: 18px weight 600 titleLarge: withUiFont( base.titleLarge?.copyWith( - fontSize: isDesktop ? 18 : 19, - fontWeight: FontWeight.w600, + fontSize: AppTypography.h2Size, + fontWeight: AppTypography.h2Weight, letterSpacing: -0.16, - height: 1.3, + height: AppTypography.h2Height, ), ), titleMedium: withUiFont( @@ -277,27 +352,29 @@ class AppTheme { height: 1.4, ), ), + // Body: 14px weight 400 bodyLarge: withUiFont( base.bodyLarge?.copyWith( - fontSize: isDesktop ? 14 : 15, - fontWeight: FontWeight.w400, - height: 1.4, + fontSize: AppTypography.bodySize, + fontWeight: AppTypography.bodyWeight, + height: AppTypography.bodyHeight, color: palette.textPrimary, ), ), bodyMedium: withUiFont( base.bodyMedium?.copyWith( - fontSize: 14, - fontWeight: FontWeight.w400, - height: 1.4, + fontSize: AppTypography.bodySize, + fontWeight: AppTypography.bodyWeight, + height: AppTypography.bodyHeight, color: palette.textSecondary, ), ), + // Meta: 12px weight 400 bodySmall: withUiFont( base.bodySmall?.copyWith( - fontSize: isDesktop ? 12 : 13, - fontWeight: FontWeight.w400, - height: 1.45, + fontSize: AppTypography.metaSize, + fontWeight: AppTypography.metaWeight, + height: AppTypography.metaHeight, color: palette.textMuted, ), ), diff --git a/lib/widgets/detail_drawer.dart b/lib/widgets/detail_drawer.dart index f5e7ee91..a1d9e0d1 100644 --- a/lib/widgets/detail_drawer.dart +++ b/lib/widgets/detail_drawer.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../models/app_models.dart'; import '../theme/app_palette.dart'; +import '../theme/app_theme.dart'; import 'status_badge.dart'; class DetailDrawer extends StatelessWidget { @@ -16,10 +17,10 @@ class DetailDrawer extends StatelessWidget { return Container( width: 360, - margin: const EdgeInsets.fromLTRB(0, 24, 24, 24), + margin: const EdgeInsets.fromLTRB(0, AppSpacing.lg, AppSpacing.lg, AppSpacing.lg), decoration: BoxDecoration( color: palette.surfacePrimary, - borderRadius: BorderRadius.circular(28), + borderRadius: BorderRadius.circular(AppRadius.dialog), border: Border.all(color: palette.strokeSoft), boxShadow: [ BoxShadow( @@ -46,23 +47,14 @@ class DetailSheet extends StatelessWidget { final mediaQuery = MediaQuery.of(context); return Container( - margin: EdgeInsets.fromLTRB(12, mediaQuery.padding.top + 12, 12, 12), + margin: EdgeInsets.fromLTRB(AppSpacing.sm, mediaQuery.padding.top + AppSpacing.sm, AppSpacing.sm, AppSpacing.sm), decoration: BoxDecoration( color: palette.surfacePrimary, - borderRadius: BorderRadius.circular(28), + borderRadius: BorderRadius.circular(AppRadius.dialog), border: Border.all(color: palette.strokeSoft), - boxShadow: [ - BoxShadow( - color: palette.shadow.withValues(alpha: 0.16), - blurRadius: 28, - offset: const Offset(0, 18), - ), - ], - ), - child: SafeArea( - top: false, - child: _DetailPanelContent(data: data, onClose: onClose), ), + constraints: const BoxConstraints(maxWidth: 480), + child: _DetailPanelContent(data: data, onClose: onClose), ); } } @@ -75,11 +67,14 @@ class _DetailPanelContent extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final palette = context.palette; + return Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( - padding: const EdgeInsets.fromLTRB(22, 22, 16, 16), + padding: const EdgeInsets.all(AppSpacing.md), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -87,81 +82,92 @@ class _DetailPanelContent extends StatelessWidget { width: 44, height: 44, decoration: BoxDecoration( - color: context.palette.accentMuted, - borderRadius: BorderRadius.circular(14), + color: palette.accentMuted, + borderRadius: BorderRadius.circular(AppRadius.button), ), - child: Icon(data.icon, color: context.palette.accent), + child: Icon(data.icon, color: palette.accent, size: 22), ), - const SizedBox(width: 14), + const SizedBox(width: AppSpacing.sm), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - data.subtitle, - style: Theme.of(context).textTheme.bodySmall, - ), - const SizedBox(height: 4), - Text( - data.title, - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 10), - StatusBadge(status: data.status, compact: true), + Text(data.title, style: theme.textTheme.headlineSmall), + const SizedBox(height: AppSpacing.xxs), + if (data.status != null) + StatusBadge(status: data.status!, compact: true), ], ), ), + const SizedBox(width: AppSpacing.xs), IconButton( onPressed: onClose, icon: const Icon(Icons.close_rounded), + iconSize: 20, + style: IconButton.styleFrom( + foregroundColor: palette.textSecondary, + backgroundColor: palette.surfaceSecondary, + ), ), ], ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 22), - child: Text( - data.description, - style: Theme.of(context).textTheme.bodyMedium, + Divider(height: 1, color: palette.strokeSoft), + if (data.subtitle != null && data.subtitle!.isNotEmpty) + Padding( + padding: const EdgeInsets.all(AppSpacing.md), + child: Text( + data.subtitle!, + style: theme.textTheme.bodySmall, + ), ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 22), - child: Wrap( - spacing: 8, - runSpacing: 8, - children: data.meta - .map( - (item) => Chip( - label: Text(item), - visualDensity: VisualDensity.compact, - ), - ) - .toList(), - ), - ), - const SizedBox(height: 18), Expanded( child: ListView( - padding: const EdgeInsets.fromLTRB(22, 0, 22, 22), + padding: const EdgeInsets.fromLTRB(AppSpacing.md, 0, AppSpacing.md, AppSpacing.md), children: [ - ...data.sections.map( - (section) => Padding( - padding: const EdgeInsets.only(bottom: 18), - child: _DetailSectionCard(section: section), + if (data.description.isNotEmpty) + Text(data.description, style: theme.textTheme.bodyMedium), + if (data.meta.isNotEmpty) ...[ + const SizedBox(height: AppSpacing.sm), + Wrap( + spacing: AppSpacing.xs, + runSpacing: AppSpacing.xxs, + children: data.meta.map((item) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs, vertical: AppSpacing.xxs), + decoration: BoxDecoration( + color: palette.surfaceSecondary, + borderRadius: BorderRadius.circular(AppRadius.badge), + ), + child: Text( + item, + style: theme.textTheme.labelSmall?.copyWith( + color: palette.textSecondary, + ), + ), + ); + }).toList(), ), - ), - Wrap( - spacing: 8, - runSpacing: 8, - children: data.actions - .map( - (action) => - OutlinedButton(onPressed: () {}, child: Text(action)), - ) - .toList(), - ), + ], + if (data.actions.isNotEmpty) ...[ + const SizedBox(height: AppSpacing.sm), + Wrap( + spacing: AppSpacing.xs, + runSpacing: AppSpacing.xs, + children: data.actions.map((action) { + return TextButton( + onPressed: () {}, + child: Text(action), + ); + }).toList(), + ), + ], + ...data.sections.map((section) { + return Padding( + padding: const EdgeInsets.only(top: AppSpacing.md), + child: _DetailSection(section: section), + ); + }), ], ), ), @@ -170,52 +176,52 @@ class _DetailPanelContent extends StatelessWidget { } } -class _DetailSectionCard extends StatelessWidget { - const _DetailSectionCard({required this.section}); +class _DetailSection extends StatelessWidget { + const _DetailSection({required this.section}); final DetailSection section; @override Widget build(BuildContext context) { + final theme = Theme.of(context); final palette = context.palette; - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(18), - border: Border.all(color: palette.strokeSoft), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(section.title, style: Theme.of(context).textTheme.titleMedium), - const SizedBox(height: 12), - ...section.items.map( - (item) => Padding( - padding: const EdgeInsets.only(bottom: 10), - child: Row( - children: [ - Expanded( - child: Text( - item.label, - style: Theme.of(context).textTheme.bodySmall, - ), - ), - const SizedBox(width: 16), - Flexible( - child: Text( - item.value, - textAlign: TextAlign.right, - style: Theme.of(context).textTheme.labelLarge, - ), - ), - ], - ), - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + section.title, + style: theme.textTheme.labelLarge?.copyWith( + color: palette.textSecondary, ), - ], - ), + ), + const SizedBox(height: AppSpacing.xs), + ...section.items.map((item) { + return Padding( + padding: const EdgeInsets.only(bottom: AppSpacing.xs), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 80, + child: Text( + item.label, + style: theme.textTheme.bodySmall?.copyWith( + color: palette.textMuted, + ), + ), + ), + Expanded( + child: Text( + item.value, + style: theme.textTheme.bodyMedium, + ), + ), + ], + ), + ); + }), + ], ); } } diff --git a/lib/widgets/gateway_connect_dialog.dart b/lib/widgets/gateway_connect_dialog.dart index 07378063..43979086 100644 --- a/lib/widgets/gateway_connect_dialog.dart +++ b/lib/widgets/gateway_connect_dialog.dart @@ -5,6 +5,7 @@ import '../i18n/app_language.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/runtime_models.dart'; import 'section_tabs.dart'; +import '../theme/app_theme.dart'; class GatewayConnectDialog extends StatefulWidget { const GatewayConnectDialog({ diff --git a/lib/widgets/metric_card.dart b/lib/widgets/metric_card.dart index 57c67771..ed2058cd 100644 --- a/lib/widgets/metric_card.dart +++ b/lib/widgets/metric_card.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../models/app_models.dart'; import '../theme/app_palette.dart'; +import '../theme/app_theme.dart'; import 'status_badge.dart'; import 'surface_card.dart'; @@ -25,7 +26,7 @@ class MetricCard extends StatelessWidget { height: 40, decoration: BoxDecoration( color: palette.accentMuted, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppRadius.card), ), child: Icon(metric.icon, color: palette.accent, size: 20), ), @@ -34,11 +35,11 @@ class MetricCard extends StatelessWidget { StatusBadge(status: metric.status!, compact: true), ], ), - const SizedBox(height: 18), + const SizedBox(height: AppSpacing.lg), Text(metric.label, style: Theme.of(context).textTheme.bodyMedium), - const SizedBox(height: 6), + const SizedBox(height: AppSpacing.xxs), Text(metric.value, style: Theme.of(context).textTheme.headlineSmall), - const SizedBox(height: 6), + const SizedBox(height: AppSpacing.xxs), Text(metric.caption, style: Theme.of(context).textTheme.bodySmall), ], ), diff --git a/lib/widgets/section_header.dart b/lib/widgets/section_header.dart index 2604a961..2aedc034 100644 --- a/lib/widgets/section_header.dart +++ b/lib/widgets/section_header.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../theme/app_theme.dart'; class SectionHeader extends StatelessWidget { const SectionHeader({ @@ -22,7 +23,7 @@ class SectionHeader extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: Theme.of(context).textTheme.titleLarge), - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.xs), Text(subtitle, style: Theme.of(context).textTheme.bodySmall), ], ), diff --git a/lib/widgets/section_tabs.dart b/lib/widgets/section_tabs.dart index ce598bc7..7aa47d79 100644 --- a/lib/widgets/section_tabs.dart +++ b/lib/widgets/section_tabs.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../theme/app_palette.dart'; +import '../theme/app_theme.dart'; enum SectionTabsSize { small, medium } @@ -23,20 +24,20 @@ class SectionTabs extends StatelessWidget { final palette = context.palette; final padding = switch (size) { SectionTabsSize.small => const EdgeInsets.symmetric( - horizontal: 12, + horizontal: AppSpacing.sm, vertical: 6, ), SectionTabsSize.medium => const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, + horizontal: AppSpacing.sm, + vertical: AppSpacing.xs, ), }; return Container( - padding: const EdgeInsets.all(4), + padding: const EdgeInsets.all(AppSpacing.xxs), decoration: BoxDecoration( color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppRadius.chip), border: Border.all(color: palette.strokeSoft), ), child: SingleChildScrollView( @@ -45,7 +46,7 @@ class SectionTabs extends StatelessWidget { children: items.map((item) { final selected = item == value; return Padding( - padding: const EdgeInsets.only(right: 4), + padding: const EdgeInsets.only(right: AppSpacing.xxs), child: _SectionTabChip( label: item, selected: selected, @@ -96,12 +97,12 @@ class _SectionTabChipState extends State<_SectionTabChip> { : _hovered ? palette.hover : Colors.transparent, - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.button), ), child: Material( color: Colors.transparent, child: InkWell( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.button), onTap: widget.onTap, child: Padding( padding: widget.padding, diff --git a/lib/widgets/sidebar_navigation.dart b/lib/widgets/sidebar_navigation.dart index f9a9af07..2242e00b 100644 --- a/lib/widgets/sidebar_navigation.dart +++ b/lib/widgets/sidebar_navigation.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../theme/app_palette.dart'; +import '../theme/app_theme.dart'; class SidebarNavigation extends StatelessWidget { const SidebarNavigation({ @@ -51,23 +52,23 @@ class SidebarNavigation extends StatelessWidget { final isCollapsed = sidebarState == AppSidebarState.collapsed; final expandedWidth = expandedWidthOverride ?? - (appLanguage == AppLanguage.zh ? 204.0 : 220.0); + (appLanguage == AppLanguage.zh ? AppSizes.sidebarExpandedWidth : 220.0); return AnimatedContainer( duration: const Duration(milliseconds: 220), curve: Curves.easeOutCubic, - width: isExpanded ? expandedWidth : 72, + width: isExpanded ? expandedWidth : AppSizes.sidebarCollapsedWidth, height: double.infinity, - margin: const EdgeInsets.fromLTRB(8, 8, 6, 8), + margin: const EdgeInsets.fromLTRB(AppSpacing.xs, AppSpacing.xs, 6, 0), decoration: BoxDecoration( color: palette.sidebar, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(AppRadius.sidebar), border: Border.all( color: palette.sidebarBorder.withValues(alpha: 0.72), ), ), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs, vertical: AppSpacing.xs), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -75,16 +76,16 @@ class SidebarNavigation extends StatelessWidget { isCollapsed: !isExpanded, onTap: isCollapsed ? onExpandFromCollapsed : null, ), - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.xs), Container(height: 1, color: palette.sidebarBorder), - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.xs), Expanded( child: ListView( padding: EdgeInsets.zero, children: [ ..._mainSections.map( (section) => Padding( - padding: const EdgeInsets.only(bottom: 4), + padding: const EdgeInsets.only(bottom: AppSpacing.xxs), child: SidebarNavItem( section: section, selected: currentSection == section, @@ -93,9 +94,9 @@ class SidebarNavigation extends StatelessWidget { ), ), ), - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.xs), Container(height: 1, color: palette.sidebarBorder), - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.xs), SidebarFooter( isCollapsed: isCollapsed, appLanguage: appLanguage, @@ -133,13 +134,13 @@ class SidebarHeader extends StatelessWidget { final palette = context.palette; final content = Container( - width: isCollapsed ? 36 : 32, - height: isCollapsed ? 36 : 32, + width: isCollapsed ? AppSizes.sidebarItemHeight : 32, + height: isCollapsed ? AppSizes.sidebarItemHeight : 32, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.button), color: palette.accentMuted, ), - child: Icon(Icons.auto_awesome_rounded, color: palette.accent, size: 18), + child: Icon(Icons.auto_awesome_rounded, color: palette.accent, size: AppSizes.sidebarIconSize), ); if (onTap == null) { @@ -149,7 +150,7 @@ class SidebarHeader extends StatelessWidget { return Tooltip( message: appText('展开导航', 'Expand sidebar'), child: InkWell( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.button), onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(vertical: 2), @@ -184,66 +185,89 @@ class _SidebarNavItemState extends State { @override Widget build(BuildContext context) { final palette = context.palette; - final active = widget.selected; - final background = active + final background = widget.selected ? palette.accentMuted : _hovered ? palette.hover : Colors.transparent; - final foreground = active ? palette.accent : palette.textSecondary; - final item = AnimatedContainer( - duration: const Duration(milliseconds: 160), - curve: Curves.easeOutCubic, - width: widget.collapsed ? null : double.infinity, - height: widget.collapsed ? 40 : 38, - decoration: BoxDecoration( - color: background, - borderRadius: BorderRadius.circular(10), - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(10), - onTap: widget.onTap, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: widget.collapsed ? 0 : 10, - vertical: 0, - ), - child: Row( - mainAxisAlignment: widget.collapsed - ? MainAxisAlignment.center - : MainAxisAlignment.start, - children: [ - Icon(widget.section.icon, color: foreground, size: 18), - if (!widget.collapsed) ...[ - const SizedBox(width: 8), - Expanded( - child: Text( - widget.section.label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of( - context, - ).textTheme.labelLarge?.copyWith(color: foreground), - ), - ), - ], - ], + return Tooltip( + message: widget.collapsed ? _sectionLabel(widget.section) : '', + child: MouseRegion( + onEnter: (_) => setState(() => _hovered = true), + onExit: (_) => setState(() => _hovered = false), + child: AnimatedContainer( + duration: const Duration(milliseconds: 160), + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(AppRadius.button), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(AppRadius.button), + onTap: widget.onTap, + child: Container( + height: AppSizes.sidebarItemHeight, + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs), + child: widget.collapsed + ? Center( + child: Icon( + _sectionIcon(widget.section), + size: AppSizes.sidebarIconSize, + color: widget.selected + ? palette.accent + : palette.textSecondary, + ), + ) + : Row( + children: [ + Icon( + _sectionIcon(widget.section), + size: AppSizes.sidebarIconSize, + color: widget.selected + ? palette.accent + : palette.textSecondary, + ), + const SizedBox(width: AppSpacing.xs), + Text( + _sectionLabel(widget.section), + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: widget.selected + ? palette.textPrimary + : palette.textSecondary, + ), + ), + ], + ), + ), ), ), ), ), ); + } - return MouseRegion( - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: widget.collapsed - ? Tooltip(message: widget.section.label, child: item) - : item, - ); + IconData _sectionIcon(WorkspaceDestination section) { + return switch (section) { + WorkspaceDestination.assistant => Icons.auto_awesome_rounded, + WorkspaceDestination.tasks => Icons.task_alt_rounded, + WorkspaceDestination.modules => Icons.extension_rounded, + WorkspaceDestination.secrets => Icons.key_rounded, + WorkspaceDestination.settings => Icons.tune_rounded, + WorkspaceDestination.account => Icons.account_circle_rounded, + }; + } + + String _sectionLabel(WorkspaceDestination section) { + return switch (section) { + WorkspaceDestination.assistant => appText('助手', 'Assistant'), + WorkspaceDestination.tasks => appText('任务', 'Tasks'), + WorkspaceDestination.modules => appText('模块', 'Modules'), + WorkspaceDestination.secrets => appText('密钥', 'Secrets'), + WorkspaceDestination.settings => appText('设置', 'Settings'), + WorkspaceDestination.account => appText('账户', 'Account'), + }; } } @@ -251,12 +275,12 @@ class SidebarFooter extends StatelessWidget { const SidebarFooter({ super.key, required this.isCollapsed, - required this.sidebarState, required this.appLanguage, required this.themeMode, required this.onToggleLanguage, required this.onOpenThemeToggle, required this.onOpenSettings, + required this.sidebarState, required this.onCycleSidebarState, required this.onOpenAccount, required this.accountName, @@ -265,12 +289,12 @@ class SidebarFooter extends StatelessWidget { }); final bool isCollapsed; - final AppSidebarState sidebarState; final AppLanguage appLanguage; final ThemeMode themeMode; final VoidCallback onToggleLanguage; final VoidCallback onOpenThemeToggle; final VoidCallback onOpenSettings; + final AppSidebarState sidebarState; final VoidCallback onCycleSidebarState; final VoidCallback onOpenAccount; final String accountName; @@ -279,205 +303,161 @@ class SidebarFooter extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); final palette = context.palette; - final themeLabel = themeMode == ThemeMode.dark - ? appText('切换浅色', 'Switch to light') - : appText('切换深色', 'Switch to dark'); - final collapseLabel = switch (sidebarState) { - AppSidebarState.expanded => appText('折叠导航', 'Collapse sidebar'), - AppSidebarState.collapsed => appText('隐藏导航', 'Hide sidebar'), - AppSidebarState.hidden => appText('展开导航', 'Expand sidebar'), - }; - final themeButton = Tooltip( - message: themeLabel, - child: IconButton( - iconSize: 18, - onPressed: onOpenThemeToggle, - icon: Icon( - themeMode == ThemeMode.dark - ? Icons.light_mode_rounded - : Icons.dark_mode_rounded, - ), - ), - ); - - final languageButton = Tooltip( - message: appText('切换语言', 'Switch language'), - child: _SidebarLanguageButton( - appLanguage: appLanguage, - compact: isCollapsed, - onPressed: onToggleLanguage, - ), - ); - - final settingsButton = Tooltip( - message: appText('打开设置', 'Open settings'), - child: IconButton( - iconSize: 18, - onPressed: onOpenSettings, - icon: const Icon(Icons.settings_rounded), - ), - ); - - final collapseButton = Tooltip( - message: collapseLabel, - child: IconButton( - iconSize: 18, - onPressed: onCycleSidebarState, - icon: Icon(switch (sidebarState) { - AppSidebarState.expanded => Icons.keyboard_double_arrow_left_rounded, - AppSidebarState.collapsed => Icons.visibility_off_outlined, - AppSidebarState.hidden => Icons.keyboard_double_arrow_right_rounded, - }), - ), - ); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (isCollapsed) - Column( - mainAxisSize: MainAxisSize.min, - children: [ - themeButton, - const SizedBox(height: 4), - languageButton, - const SizedBox(height: 4), - settingsButton, - const SizedBox(height: 4), - collapseButton, - ], - ) - else - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _SidebarFooterActionTile( - icon: themeMode == ThemeMode.dark - ? Icons.light_mode_rounded - : Icons.dark_mode_rounded, - label: themeLabel, - onTap: onOpenThemeToggle, - ), - const SizedBox(height: 4), - _SidebarFooterActionTile( - icon: Icons.translate_rounded, - label: appText('语言', 'Language'), - trailingText: appLanguage == AppLanguage.zh ? '中文' : 'EN', - onTap: onToggleLanguage, - ), - const SizedBox(height: 4), - _SidebarFooterActionTile( - icon: Icons.settings_rounded, - label: appText('打开设置', 'Open settings'), - onTap: onOpenSettings, - ), - const SizedBox(height: 4), - _SidebarFooterActionTile( - icon: switch (sidebarState) { - AppSidebarState.expanded => - Icons.keyboard_double_arrow_left_rounded, - AppSidebarState.collapsed => Icons.visibility_off_outlined, - AppSidebarState.hidden => - Icons.keyboard_double_arrow_right_rounded, - }, - label: collapseLabel, - onTap: onCycleSidebarState, - ), - ], + if (isCollapsed) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container(height: 1, color: palette.sidebarBorder), + const SizedBox(height: AppSpacing.xs), + _SidebarLanguageButton( + appLanguage: appLanguage, + compact: true, + onPressed: onToggleLanguage, ), - const SizedBox(height: 12), - if (isCollapsed) - Tooltip( - message: appText('账号', 'Account'), - child: InkWell( - borderRadius: BorderRadius.circular(10), - onTap: onOpenAccount, - child: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 8), - decoration: BoxDecoration( - color: accountSelected - ? palette.accentMuted - : palette.surfaceSecondary, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: palette.strokeSoft), - ), - child: const Icon(Icons.account_circle_rounded, size: 20), - ), - ), - ) - else + const SizedBox(height: AppSpacing.xs), + _SidebarActionButton( + icon: themeMode == ThemeMode.dark + ? Icons.dark_mode_rounded + : themeMode == ThemeMode.light + ? Icons.light_mode_rounded + : Icons.brightness_auto_rounded, + tooltip: appText('切换主题', 'Toggle theme'), + onPressed: onOpenThemeToggle, + ), + const SizedBox(height: AppSpacing.xs), + _SidebarActionButton( + icon: _sidebarStateIcon(sidebarState), + tooltip: _sidebarStateLabel(sidebarState), + onPressed: onCycleSidebarState, + ), + const SizedBox(height: AppSpacing.xs), _SidebarAccountTile( selected: accountSelected, onTap: onOpenAccount, name: accountName, subtitle: accountSubtitle, ), + ], + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Container(height: 1, color: palette.sidebarBorder), + const SizedBox(height: AppSpacing.xs), + _SidebarLanguageButton( + appLanguage: appLanguage, + compact: false, + onPressed: onToggleLanguage, + ), + const SizedBox(height: AppSpacing.xs), + Row( + children: [ + Expanded( + child: _SidebarActionButton( + icon: themeMode == ThemeMode.dark + ? Icons.dark_mode_rounded + : themeMode == ThemeMode.light + ? Icons.light_mode_rounded + : Icons.brightness_auto_rounded, + label: appText('主题', 'Theme'), + onPressed: onOpenThemeToggle, + ), + ), + const SizedBox(width: AppSpacing.xs), + _SidebarActionButton( + icon: _sidebarStateIcon(sidebarState), + tooltip: _sidebarStateLabel(sidebarState), + onPressed: onCycleSidebarState, + ), + ], + ), + const SizedBox(height: AppSpacing.xs), + _SidebarAccountTile( + selected: accountSelected, + onTap: onOpenAccount, + name: accountName, + subtitle: accountSubtitle, + ), ], ); } + + IconData _sidebarStateIcon(AppSidebarState state) { + return switch (state) { + AppSidebarState.expanded => Icons.sidebar_rounded, + AppSidebarState.collapsed => Icons.menu_rounded, + }; + } + + String _sidebarStateLabel(AppSidebarState state) { + return switch (state) { + AppSidebarState.expanded => appText('收起侧边栏', 'Collapse sidebar'), + AppSidebarState.collapsed => appText('展开侧边栏', 'Expand sidebar'), + }; + } } -class _SidebarFooterActionTile extends StatefulWidget { - const _SidebarFooterActionTile({ +class _SidebarActionButton extends StatefulWidget { + const _SidebarActionButton({ required this.icon, - required this.label, - required this.onTap, + this.label, + this.tooltip, + required this.onPressed, this.trailingText, }); final IconData icon; - final String label; - final VoidCallback onTap; + final String? label; + final String? tooltip; + final VoidCallback onPressed; final String? trailingText; @override - State<_SidebarFooterActionTile> createState() => - _SidebarFooterActionTileState(); + State<_SidebarActionButton> createState() => _SidebarActionButtonState(); } -class _SidebarFooterActionTileState extends State<_SidebarFooterActionTile> { +class _SidebarActionButtonState extends State<_SidebarActionButton> { bool _hovered = false; @override Widget build(BuildContext context) { final palette = context.palette; + final background = _hovered ? palette.hover : Colors.transparent; - return MouseRegion( - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: Align( - alignment: Alignment.centerLeft, + if (widget.label != null) { + return MouseRegion( + onEnter: (_) => setState(() => _hovered = true), + onExit: (_) => setState(() => _hovered = false), child: AnimatedContainer( duration: const Duration(milliseconds: 160), decoration: BoxDecoration( - color: _hovered ? palette.hover : Colors.transparent, - borderRadius: BorderRadius.circular(10), + color: background, + borderRadius: BorderRadius.circular(AppRadius.button), ), child: Material( color: Colors.transparent, child: InkWell( - borderRadius: BorderRadius.circular(10), - onTap: widget.onTap, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + borderRadius: BorderRadius.circular(AppRadius.button), + onTap: widget.onPressed, + child: Container( + height: AppSizes.sidebarItemHeight, + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs), child: Row( - mainAxisSize: MainAxisSize.min, children: [ - Icon(widget.icon, size: 18, color: palette.textSecondary), - const SizedBox(width: 8), - Flexible( - child: Text( - widget.label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.labelLarge, - ), + Icon(widget.icon, size: AppSizes.sidebarIconSize, color: palette.textSecondary), + const SizedBox(width: AppSpacing.xs), + Text( + widget.label!, + style: Theme.of(context).textTheme.labelLarge, ), if (widget.trailingText != null) ...[ - const SizedBox(width: 8), + const Spacer(), Text( widget.trailingText!, style: Theme.of(context).textTheme.bodySmall?.copyWith( @@ -492,6 +472,35 @@ class _SidebarFooterActionTileState extends State<_SidebarFooterActionTile> { ), ), ), + ); + } + + return Tooltip( + message: widget.tooltip ?? '', + child: MouseRegion( + onEnter: (_) => setState(() => _hovered = true), + onExit: (_) => setState(() => _hovered = false), + child: AnimatedContainer( + duration: const Duration(milliseconds: 160), + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(AppRadius.button), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(AppRadius.button), + onTap: widget.onPressed, + child: Container( + height: AppSizes.sidebarItemHeight, + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs), + child: Center( + child: Icon(widget.icon, size: AppSizes.sidebarIconSize, color: palette.textSecondary), + ), + ), + ), + ), + ), ), ); } @@ -535,27 +544,28 @@ class _SidebarAccountTileState extends State<_SidebarAccountTile> { duration: const Duration(milliseconds: 160), decoration: BoxDecoration( color: background, - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.button), ), child: Material( color: Colors.transparent, child: InkWell( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.button), onTap: widget.onTap, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: Container( + height: AppSizes.sidebarItemHeight, + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs), child: Row( mainAxisSize: MainAxisSize.max, children: [ CircleAvatar( - radius: 16, + radius: 14, child: Text( widget.name.trim().isEmpty ? 'X' : widget.name.trim().substring(0, 1).toUpperCase(), ), ), - const SizedBox(width: 8), + const SizedBox(width: AppSpacing.xs), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -609,13 +619,13 @@ class _SidebarLanguageButtonState extends State<_SidebarLanguageButton> { @override Widget build(BuildContext context) { final palette = context.palette; - final size = widget.compact ? 36.0 : 44.0; + final size = widget.compact ? AppSizes.sidebarItemHeight : 44.0; return MouseRegion( onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: InkWell( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.button), onTap: widget.onPressed, child: AnimatedContainer( duration: const Duration(milliseconds: 160), @@ -624,7 +634,7 @@ class _SidebarLanguageButtonState extends State<_SidebarLanguageButton> { alignment: Alignment.center, decoration: BoxDecoration( color: _hovered ? palette.hover : palette.surfaceSecondary, - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.button), border: Border.all(color: palette.strokeSoft), ), child: Text( diff --git a/lib/widgets/status_badge.dart b/lib/widgets/status_badge.dart index 3350e587..cfa9a326 100644 --- a/lib/widgets/status_badge.dart +++ b/lib/widgets/status_badge.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../models/app_models.dart'; import '../theme/app_palette.dart'; +import '../theme/app_theme.dart'; class StatusBadge extends StatelessWidget { const StatusBadge({super.key, required this.status, this.compact = false}); @@ -31,12 +32,12 @@ class StatusBadge extends StatelessWidget { return Container( padding: EdgeInsets.symmetric( - horizontal: compact ? 8 : 10, + horizontal: compact ? AppSpacing.xs : 10, vertical: compact ? 4 : 6, ), decoration: BoxDecoration( color: tone.$1, - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(AppRadius.badge), ), child: Text( status.label, diff --git a/lib/widgets/surface_card.dart b/lib/widgets/surface_card.dart index a5e23292..9d9ccbf6 100644 --- a/lib/widgets/surface_card.dart +++ b/lib/widgets/surface_card.dart @@ -1,14 +1,15 @@ import 'package:flutter/material.dart'; import '../theme/app_palette.dart'; +import '../theme/app_theme.dart'; class SurfaceCard extends StatefulWidget { const SurfaceCard({ super.key, required this.child, - this.padding = const EdgeInsets.all(16), + this.padding = const EdgeInsets.all(AppSpacing.md), this.onTap, - this.borderRadius = 16, + this.borderRadius = AppRadius.card, this.color, }); diff --git a/lib/widgets/top_bar.dart b/lib/widgets/top_bar.dart index 18d584ab..cfba13c7 100644 --- a/lib/widgets/top_bar.dart +++ b/lib/widgets/top_bar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../theme/app_theme.dart'; class TopBar extends StatelessWidget { const TopBar({ @@ -23,9 +24,9 @@ class TopBar extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: Theme.of(context).textTheme.headlineSmall), - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.xs), Text(subtitle, style: Theme.of(context).textTheme.bodyMedium), - if (trailing != null) ...[const SizedBox(height: 16), trailing!], + if (trailing != null) ...[const SizedBox(height: AppSpacing.md), trailing!], ], ); } @@ -38,13 +39,13 @@ class TopBar extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: Theme.of(context).textTheme.headlineSmall), - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.xs), Text(subtitle, style: Theme.of(context).textTheme.bodyMedium), ], ), ), if (trailing != null) ...[ - const SizedBox(width: 24), + const SizedBox(width: AppSpacing.lg), Flexible(child: trailing!), ], ],