feat: refine mobile app visual design
This commit is contained in:
parent
be5e2e222f
commit
a77bfc8c78
@ -3,18 +3,21 @@ import 'package:flutter/material.dart';
|
||||
class AppTheme {
|
||||
static const EdgeInsets pagePadding = EdgeInsets.fromLTRB(20, 16, 20, 24);
|
||||
static const EdgeInsets panelPadding = EdgeInsets.all(16);
|
||||
static const BorderRadius panelRadius = BorderRadius.all(Radius.circular(24));
|
||||
static const BorderRadius panelRadius = BorderRadius.all(Radius.circular(22));
|
||||
|
||||
static ThemeData build() {
|
||||
const background = Color(0xFF09111C);
|
||||
const surface = Color(0xFF111C2C);
|
||||
const surfaceHigh = Color(0xFF182538);
|
||||
const surfaceHighest = Color(0xFF1E2E45);
|
||||
const outline = Color(0xFF2C405F);
|
||||
const outlineVariant = Color(0xFF22324A);
|
||||
const accent = Color(0xFF67CFFF);
|
||||
const secondary = Color(0xFF7AA6FF);
|
||||
const tertiary = Color(0xFF77E3CF);
|
||||
const background = Color(0xFF05070A);
|
||||
const surface = Color(0xFF101419);
|
||||
const surfaceHigh = Color(0xFF151B21);
|
||||
const surfaceHighest = Color(0xFF1A222B);
|
||||
const outline = Color(0xFF3A4652);
|
||||
const outlineVariant = Color(0xFF252E37);
|
||||
const accent = Color(0xFFC2A574);
|
||||
const secondary = Color(0xFF8EA3B8);
|
||||
const tertiary = Color(0xFF8EB8A7);
|
||||
const onSurface = Color(0xFFF3EFE7);
|
||||
const subText = Color(0xFFBBB3A7);
|
||||
const mutedText = Color(0xFF91897E);
|
||||
|
||||
final scheme = const ColorScheme.dark().copyWith(
|
||||
primary: accent,
|
||||
@ -26,6 +29,9 @@ class AppTheme {
|
||||
surfaceContainerHighest: surfaceHighest,
|
||||
outline: outline,
|
||||
outlineVariant: outlineVariant,
|
||||
onPrimary: onSurface,
|
||||
onSecondary: onSurface,
|
||||
onSurface: onSurface,
|
||||
error: const Color(0xFFFF7575),
|
||||
);
|
||||
|
||||
@ -36,6 +42,8 @@ class AppTheme {
|
||||
scaffoldBackgroundColor: background,
|
||||
canvasColor: background,
|
||||
splashFactory: InkSparkle.splashFactory,
|
||||
highlightColor: Colors.transparent,
|
||||
hoverColor: Colors.transparent,
|
||||
);
|
||||
|
||||
return base.copyWith(
|
||||
@ -44,28 +52,51 @@ class AppTheme {
|
||||
elevation: 0,
|
||||
backgroundColor: background,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
titleTextStyle: base.textTheme.titleLarge?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: onSurface,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.45,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Color(0xFFD7D0C5)),
|
||||
),
|
||||
textTheme: base.textTheme.copyWith(
|
||||
headlineSmall: base.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.4,
|
||||
color: onSurface,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1.02,
|
||||
letterSpacing: -1.05,
|
||||
),
|
||||
titleMedium: base.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: onSurface,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.3,
|
||||
),
|
||||
titleSmall: base.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: onSurface,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.2,
|
||||
),
|
||||
labelLarge: base.textTheme.labelLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: const Color(0xFFD1C2A7),
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 11,
|
||||
letterSpacing: 1.7,
|
||||
),
|
||||
labelMedium: base.textTheme.labelMedium?.copyWith(
|
||||
color: mutedText,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.4,
|
||||
),
|
||||
bodyMedium: base.textTheme.bodyMedium?.copyWith(
|
||||
color: subText,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.38,
|
||||
),
|
||||
bodySmall: base.textTheme.bodySmall?.copyWith(
|
||||
color: const Color(0xFFA4B4CB),
|
||||
color: mutedText,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
cardTheme: const CardThemeData(
|
||||
@ -80,57 +111,93 @@ class AppTheme {
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: surface,
|
||||
hintStyle: const TextStyle(color: Color(0xFF8195B2)),
|
||||
labelStyle: const TextStyle(color: Color(0xFFAFC1DB)),
|
||||
fillColor: const Color(0xFF0C1014),
|
||||
hintStyle: const TextStyle(color: mutedText),
|
||||
labelStyle: const TextStyle(
|
||||
color: subText,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: const BorderSide(color: outlineVariant),
|
||||
borderSide: const BorderSide(color: outlineVariant, width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: const BorderSide(color: outlineVariant),
|
||||
borderSide: const BorderSide(color: outlineVariant, width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: const BorderSide(color: accent),
|
||||
borderSide: const BorderSide(color: accent, width: 1.15),
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderSide: const BorderSide(color: outlineVariant, width: 1),
|
||||
),
|
||||
),
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size(0, 48),
|
||||
backgroundColor: accent,
|
||||
foregroundColor: const Color(0xFF05111D),
|
||||
backgroundColor: const Color(0xFFD6CBC0),
|
||||
foregroundColor: const Color(0xFF14100C),
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.15,
|
||||
),
|
||||
elevation: 0,
|
||||
overlayColor: const Color(0x140F0C09),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(0, 48),
|
||||
side: const BorderSide(color: outline),
|
||||
foregroundColor: const Color(0xFFE8F1FF),
|
||||
side: const BorderSide(color: outline, width: 1),
|
||||
foregroundColor: const Color(0xFFEAE2D7),
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.15,
|
||||
),
|
||||
backgroundColor: const Color(0xFF14191F),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFFD2BF9A),
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
),
|
||||
iconButtonTheme: IconButtonThemeData(
|
||||
style: IconButton.styleFrom(foregroundColor: const Color(0xFFD7E4F8)),
|
||||
style: IconButton.styleFrom(
|
||||
foregroundColor: const Color(0xFFE0D9CF),
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
),
|
||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||
backgroundColor: accent,
|
||||
foregroundColor: Color(0xFF05111D),
|
||||
backgroundColor: Color(0xFFB89460),
|
||||
foregroundColor: Color(0xFF120F0B),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(18)),
|
||||
),
|
||||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: surfaceHighest,
|
||||
contentTextStyle: const TextStyle(color: Color(0xFFF2F7FF)),
|
||||
contentTextStyle: const TextStyle(color: onSurface),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
||||
),
|
||||
dividerTheme: const DividerThemeData(color: outlineVariant, thickness: 1),
|
||||
|
||||
@ -2,28 +2,77 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'app_theme.dart';
|
||||
|
||||
enum AppPanelTone { primary, subdued, emphasis }
|
||||
|
||||
class AppPanel extends StatelessWidget {
|
||||
const AppPanel({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.padding = AppTheme.panelPadding,
|
||||
this.borderRadius = AppTheme.panelRadius,
|
||||
this.tone = AppPanelTone.primary,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final BorderRadiusGeometry borderRadius;
|
||||
final AppPanelTone tone;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final palette = switch (tone) {
|
||||
AppPanelTone.primary => (
|
||||
base: colorScheme.surfaceContainerHighest,
|
||||
edge: colorScheme.outlineVariant,
|
||||
glow: const Color(0x22000000),
|
||||
),
|
||||
AppPanelTone.subdued => (
|
||||
base: colorScheme.surfaceContainerHigh,
|
||||
edge: colorScheme.outlineVariant.withValues(alpha: 0.8),
|
||||
glow: const Color(0x18000000),
|
||||
),
|
||||
AppPanelTone.emphasis => (
|
||||
base: const Color(0xFF161B21),
|
||||
edge: const Color(0xFF4B4338),
|
||||
glow: const Color(0x14000000),
|
||||
),
|
||||
};
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color.alphaBlend(
|
||||
Colors.white.withValues(alpha: 0.035),
|
||||
palette.base,
|
||||
),
|
||||
palette.base,
|
||||
],
|
||||
),
|
||||
borderRadius: borderRadius,
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
border: Border.all(color: palette.edge),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: palette.glow,
|
||||
blurRadius: 22,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: borderRadius,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.white.withValues(alpha: 0.02), Colors.transparent],
|
||||
),
|
||||
),
|
||||
child: Padding(padding: padding, child: child),
|
||||
),
|
||||
child: Padding(padding: padding, child: child),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -45,10 +94,22 @@ class AppSectionHeader extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(eyebrow, style: Theme.of(context).textTheme.labelLarge),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(width: 28, height: 1, color: const Color(0xFF766855)),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
eyebrow.toUpperCase(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelLarge?.copyWith(color: const Color(0xFFD0C0A3)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(title, style: Theme.of(context).textTheme.headlineSmall),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 6),
|
||||
Text(subtitle, style: Theme.of(context).textTheme.bodySmall),
|
||||
],
|
||||
);
|
||||
@ -70,9 +131,29 @@ class AppEmptyState extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppPanel(
|
||||
tone: AppPanelTone.subdued,
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, size: 56, color: Theme.of(context).colorScheme.primary),
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.09),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.18),
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 28,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
title,
|
||||
@ -105,18 +186,23 @@ class StatusPill extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backgroundColor = Color.alphaBlend(
|
||||
color.withValues(alpha: 0.18),
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
);
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.14),
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(color: color.withValues(alpha: 0.24)),
|
||||
border: Border.all(color: color.withValues(alpha: 0.28)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 16, color: color),
|
||||
Icon(icon, size: 14, color: color),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
|
||||
@ -356,7 +356,7 @@ class _ProjectListPageState extends ConsumerState<ProjectListPage> {
|
||||
Text(
|
||||
'Recent sessions',
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
color: const Color(0xFFD0C0A3),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@ -392,6 +392,7 @@ class _ProjectListPageState extends ConsumerState<ProjectListPage> {
|
||||
final isCompact = MediaQuery.sizeOf(context).width < 420;
|
||||
|
||||
return AppPanel(
|
||||
tone: AppPanelTone.emphasis,
|
||||
key: const Key('agent_connection_panel'),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -400,6 +401,11 @@ class _ProjectListPageState extends ConsumerState<ProjectListPage> {
|
||||
'Agent base URL',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Remote endpoint',
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (isCompact) ...[
|
||||
TextField(
|
||||
@ -443,9 +449,21 @@ class _ProjectListPageState extends ConsumerState<ProjectListPage> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Project requests use this base origin: ${baseUri.toString()}.',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
Container(
|
||||
key: const Key('agent_connection_readout'),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surface.withValues(alpha: 0.72),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: const Color(0xFF3A342C)),
|
||||
),
|
||||
child: Text(
|
||||
'Project requests use this base origin: ${baseUri.toString()}.',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -481,7 +499,20 @@ class _ProjectListPageState extends ConsumerState<ProjectListPage> {
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.folder_copy_outlined),
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x22C2A574),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: const Color(0xFF443A2E)),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.folder_copy_outlined,
|
||||
size: 20,
|
||||
color: const Color(0xFFC2A574),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@ -671,8 +702,13 @@ class _ProjectSessionRow extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.8),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outlineVariant.withValues(alpha: 0.9),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 6, 8),
|
||||
|
||||
@ -184,6 +184,7 @@ class _SessionListPageState extends ConsumerState<SessionListPage> {
|
||||
final isCompact = MediaQuery.sizeOf(context).width < 420;
|
||||
|
||||
return AppPanel(
|
||||
tone: AppPanelTone.emphasis,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -191,6 +192,11 @@ class _SessionListPageState extends ConsumerState<SessionListPage> {
|
||||
'Agent base URL',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Runtime endpoint',
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (isCompact) ...[
|
||||
TextField(
|
||||
@ -234,9 +240,21 @@ class _SessionListPageState extends ConsumerState<SessionListPage> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Session requests use this base origin: ${baseUri.toString()}.',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
Container(
|
||||
key: const Key('agent_connection_readout'),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surface.withValues(alpha: 0.72),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: const Color(0xFF3A342C)),
|
||||
),
|
||||
child: Text(
|
||||
'Session requests use this base origin: ${baseUri.toString()}.',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -272,7 +290,20 @@ class _SessionListPageState extends ConsumerState<SessionListPage> {
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.memory_outlined),
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x229E8A6B),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: const Color(0xFF443A2E)),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.memory_outlined,
|
||||
size: 20,
|
||||
color: const Color(0xFFB8A180),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
|
||||
@ -207,6 +207,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
Future<void> _showDiagnostics() {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
backgroundColor: const Color(0xFF13191F),
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
return SafeArea(
|
||||
@ -230,10 +231,9 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
color: const Color(0xFF0E1217),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFF2D241B)),
|
||||
),
|
||||
child: _diagnosticLog.entries.isEmpty
|
||||
? Text(
|
||||
@ -270,6 +270,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
Future<void> _showToolsSheet() {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
backgroundColor: const Color(0xFF13191F),
|
||||
builder: (context) {
|
||||
return SafeArea(
|
||||
child: AnimatedBuilder(
|
||||
@ -367,23 +368,27 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
appBar: AppBar(
|
||||
toolbarHeight: 44,
|
||||
titleSpacing: 0,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
widget.session.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
Text(
|
||||
workingDirectory,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
title: Container(
|
||||
key: const Key('terminal_header_panel'),
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
widget.session.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
Text(
|
||||
workingDirectory,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
AnimatedBuilder(
|
||||
@ -395,6 +400,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
final modeLabel = '$mode | ${controller.liveLines.length} lines';
|
||||
|
||||
return Row(
|
||||
key: const Key('terminal_status_summary'),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
@ -402,6 +408,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
? controller.enterScrollback
|
||||
: controller.jumpToLive,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFFD8C4A0),
|
||||
minimumSize: const Size(0, 32),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
@ -435,11 +442,16 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
child: Container(
|
||||
key: const Key('terminal_surface_panel'),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.zero,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
const Color(0xFF090B0E),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.zero,
|
||||
border: Border.all(color: const Color(0xFF332B22)),
|
||||
),
|
||||
child: TerminalView(
|
||||
terminal,
|
||||
@ -472,6 +484,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
child: Column(
|
||||
children: [
|
||||
AppPanel(
|
||||
tone: AppPanelTone.subdued,
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -510,8 +523,9 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
constraints: const BoxConstraints(maxHeight: 64),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
color: const Color(0xFF0D1115),
|
||||
borderRadius: BorderRadius.zero,
|
||||
border: Border.all(color: const Color(0xFF2A231B)),
|
||||
),
|
||||
child: ListView.separated(
|
||||
key: const Key('terminal_scrollback_list'),
|
||||
@ -547,8 +561,9 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
color: const Color(0xFF0D1115),
|
||||
borderRadius: BorderRadius.zero,
|
||||
border: Border.all(color: const Color(0xFF2A231B)),
|
||||
),
|
||||
child: isCompact
|
||||
? _buildCompactHistoryActions(context)
|
||||
@ -559,6 +574,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
AppPanel(
|
||||
tone: AppPanelTone.emphasis,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
@ -570,7 +586,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
Icon(
|
||||
Icons.pause_circle_outline,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
color: const Color(0xFFC2A574),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
@ -675,6 +691,7 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
Widget _buildCommandDeck(BuildContext context, bool isCompact) {
|
||||
return AppPanel(
|
||||
key: const Key('terminal_command_deck'),
|
||||
tone: AppPanelTone.emphasis,
|
||||
padding: const EdgeInsets.fromLTRB(8, 6, 8, 6),
|
||||
borderRadius: BorderRadius.zero,
|
||||
child: AnimatedBuilder(
|
||||
@ -685,6 +702,13 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Command deck',
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: const Color(0xFFD1C2A7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
@ -698,6 +722,9 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
? () => _sendQuickKey(quickKey)
|
||||
: null,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFFE8DED1),
|
||||
backgroundColor: const Color(0xFF151A20),
|
||||
side: const BorderSide(color: Color(0xFF3F3428)),
|
||||
minimumSize: const Size(0, 34),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
@ -746,6 +773,10 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
key: const Key('terminal_direct_input_toggle'),
|
||||
onPressed: _canSendInput ? _toggleDirectInput : null,
|
||||
visualDensity: VisualDensity.compact,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF241F1A),
|
||||
foregroundColor: const Color(0xFFD7C4A0),
|
||||
),
|
||||
icon: Icon(
|
||||
_isDirectInputEnabled
|
||||
? Icons.keyboard_hide
|
||||
@ -760,6 +791,10 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
key: const Key('terminal_toggle_actions_button'),
|
||||
onPressed: _showToolsSheet,
|
||||
visualDensity: VisualDensity.compact,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF241F1A),
|
||||
foregroundColor: const Color(0xFFD7C4A0),
|
||||
),
|
||||
icon: const Icon(Icons.tune),
|
||||
tooltip: 'Show tools',
|
||||
),
|
||||
@ -797,6 +832,10 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
key: const Key('terminal_direct_input_toggle'),
|
||||
onPressed: _canSendInput ? _toggleDirectInput : null,
|
||||
visualDensity: VisualDensity.compact,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF241F1A),
|
||||
foregroundColor: const Color(0xFFD7C4A0),
|
||||
),
|
||||
icon: Icon(
|
||||
_isDirectInputEnabled
|
||||
? Icons.keyboard_hide
|
||||
@ -811,6 +850,10 @@ class _TerminalPageState extends ConsumerState<TerminalPage> {
|
||||
key: const Key('terminal_toggle_actions_button'),
|
||||
onPressed: _showToolsSheet,
|
||||
visualDensity: VisualDensity.compact,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF241F1A),
|
||||
foregroundColor: const Color(0xFFD7C4A0),
|
||||
),
|
||||
icon: const Icon(Icons.tune),
|
||||
tooltip: 'Show tools',
|
||||
),
|
||||
|
||||
@ -39,6 +39,7 @@ void main() {
|
||||
|
||||
expect(find.text('Projects'), findsOneWidget);
|
||||
expect(find.byKey(const Key('project_page_header')), findsOneWidget);
|
||||
expect(find.byKey(const Key('agent_connection_readout')), findsOneWidget);
|
||||
expect(find.text('TermRemoteCtl'), findsOneWidget);
|
||||
expect(find.text(r'C:\repo\termremotectl'), findsOneWidget);
|
||||
expect(
|
||||
|
||||
@ -32,10 +32,13 @@ void main() {
|
||||
expect(find.text('Projects'), findsOneWidget);
|
||||
expect(materialApp.theme?.brightness, Brightness.dark);
|
||||
expect(materialApp.theme?.scaffoldBackgroundColor, isNot(Colors.white));
|
||||
expect(materialApp.theme?.scaffoldBackgroundColor, const Color(0xFF05070A));
|
||||
expect(materialApp.theme?.colorScheme.primary, const Color(0xFFC2A574));
|
||||
expect(find.byKey(const Key('project_page_header')), findsOneWidget);
|
||||
expect(find.text('Agent base URL'), findsOneWidget);
|
||||
expect(agentUrlField.controller?.text, 'http://10.0.2.2:5067');
|
||||
expect(agentUrlField.decoration?.hintText, 'http://10.0.2.2:5067');
|
||||
expect(find.byKey(const Key('agent_connection_readout')), findsOneWidget);
|
||||
expect(
|
||||
find.text('Project requests use this base origin: http://10.0.2.2:5067.'),
|
||||
findsOneWidget,
|
||||
@ -67,8 +70,10 @@ void main() {
|
||||
expect(sessionRepository.lastCreatedProjectId, 'project-1');
|
||||
expect(find.text('codex-main'), findsOneWidget);
|
||||
expect(find.text(r'C:\repo\codex-main'), findsOneWidget);
|
||||
expect(find.byKey(const Key('terminal_header_panel')), findsOneWidget);
|
||||
expect(find.byKey(const Key('terminal_surface_panel')), findsOneWidget);
|
||||
expect(find.byKey(const Key('terminal_command_deck')), findsOneWidget);
|
||||
expect(find.byKey(const Key('terminal_status_summary')), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const Key('terminal_toggle_actions_button')),
|
||||
findsOneWidget,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user