refactor(ui): modernize design system with consistent spacing and typography
This commit is contained in:
parent
b1aaa1b8ee
commit
19f49acdd6
@ -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;
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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),
|
||||
],
|
||||
),
|
||||
|
||||
@ -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),
|
||||
],
|
||||
),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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!),
|
||||
],
|
||||
],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user