refactor(ui): modernize design system with consistent spacing and typography

This commit is contained in:
Haitao Pan 2026-03-13 19:12:27 +08:00
parent b1aaa1b8ee
commit 19f49acdd6
17 changed files with 578 additions and 490 deletions

View File

@ -57,6 +57,7 @@ class _AppShellState extends State<AppShell> {
final controller = widget.controller;
return Scaffold(
body: SafeArea(
bottom: false,
child: LayoutBuilder(
builder: (context, constraints) {
final palette = context.palette;

View File

@ -28,7 +28,7 @@ class _AccountPageState extends State<AccountPage> {
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: [

View File

@ -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<AssistantPage> {
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<AssistantPage> {
});
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,
),
);
}

View File

@ -62,7 +62,7 @@ class _ModulesPageState extends State<ModulesPage> {
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: [

View File

@ -35,7 +35,7 @@ class _SecretsPageState extends State<SecretsPage> {
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: [

View File

@ -70,7 +70,7 @@ class _SettingsPageState extends State<SettingsPage> {
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: [

View File

@ -67,7 +67,7 @@ class _TasksPageState extends State<TasksPage> {
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: [

View File

@ -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,
),
),

View File

@ -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,
),
),
],
),
);
}),
],
);
}
}

View File

@ -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({

View File

@ -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),
],
),

View File

@ -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),
],
),

View File

@ -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,

View File

@ -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<SidebarNavItem> {
@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(

View File

@ -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,

View File

@ -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,
});

View File

@ -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!),
],
],