Refine cross-platform workspace theme

This commit is contained in:
Haitao Pan 2026-03-19 11:20:09 +08:00
parent 5d0240def0
commit 545ffd6549
8 changed files with 289 additions and 228 deletions

View File

@ -280,6 +280,7 @@ class _AppShellState extends State<AppShell> {
palette.chromeBackground,
palette.canvas,
],
stops: const [0.0, 0.68],
),
),
child: Stack(
@ -300,8 +301,8 @@ class _AppShellState extends State<AppShell> {
alpha:
brightness ==
Brightness.dark
? 0.10
: 0.58,
? 0.14
: 0.42,
),
palette.chromeHighlight
.withValues(alpha: 0),
@ -326,8 +327,8 @@ class _AppShellState extends State<AppShell> {
alpha:
brightness ==
Brightness.dark
? 0.10
: 0.36,
? 0.14
: 0.24,
),
palette.chromeInset.withValues(
alpha: 0,

View File

@ -14,6 +14,7 @@ import '../../features/tasks/tasks_page.dart';
import '../../i18n/app_language.dart';
import '../../models/app_models.dart';
import '../../runtime/runtime_models.dart';
import '../../theme/app_palette.dart';
import '../../widgets/detail_drawer.dart';
import '../../widgets/gateway_connect_dialog.dart';
@ -37,21 +38,10 @@ extension on MobileShellTab {
};
}
const _background = Color(0xFFF3EFF6);
const _surface = Colors.white;
const _surfaceSoft = Color(0xFFF7F4FB);
const _stroke = Color(0xFFE3DDEE);
const _textPrimary = Color(0xFF101113);
const _textSecondary = Color(0xFF8A8694);
const _accentStart = Color(0xFF7C88F8);
const _accentEnd = Color(0xFF6757EF);
const _accentSoft = Color(0xFFD9D5FA);
const _blueSoft = Color(0xFFDCE4F1);
const _blueLine = Color(0xFF6285A6);
const _greenSoft = Color(0xFFDCEFE2);
const _greenLine = Color(0xFF62C56A);
const _orangeSoft = Color(0xFFF5E7D9);
const _orangeLine = Color(0xFFE1913E);
const _tealSoft = Color(0xFFDDF3EF);
const _tealLine = Color(0xFF49A892);
const _violetSoft = Color(0xFFECE2FF);
const _violetLine = Color(0xFF7A61B6);
class MobileShell extends StatefulWidget {
const MobileShell({super.key, required this.controller});
@ -242,20 +232,32 @@ class _MobileShellState extends State<MobileShell> {
'mobile-shell-${widget.controller.destination.name}',
);
final detailPanel = widget.controller.detailPanel;
final palette = context.palette;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: _background,
backgroundColor: palette.canvas,
body: Stack(
children: [
const Positioned(
Positioned(
top: 100,
left: -80,
child: _GlowOrb(size: 220, color: Color(0x1A8C89FF)),
child: _GlowOrb(
size: 220,
color: palette.accentMuted.withValues(
alpha: isDark ? 0.36 : 0.6,
),
),
),
const Positioned(
Positioned(
right: -90,
bottom: 220,
child: _GlowOrb(size: 260, color: Color(0x143AB08F)),
child: _GlowOrb(
size: 260,
color: palette.chromeHighlight.withValues(
alpha: isDark ? 0.16 : 0.4,
),
),
),
SafeArea(
child: Padding(
@ -264,11 +266,21 @@ class _MobileShellState extends State<MobileShell> {
children: [
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(32),
borderRadius: BorderRadius.circular(28),
child: DecoratedBox(
decoration: BoxDecoration(
color: _surface.withValues(alpha: 0.94),
border: Border.all(color: _stroke),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(
alpha: isDark ? 0.16 : 0.9,
),
palette.chromeSurface.withValues(alpha: 0.94),
],
),
border: Border.all(color: palette.chromeStroke),
boxShadow: [palette.chromeShadowAmbient],
),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 220),
@ -298,7 +310,7 @@ class _MobileShellState extends State<MobileShell> {
child: GestureDetector(
onTap: widget.controller.closeDetail,
child: Container(
color: Colors.black.withValues(alpha: 0.12),
color: Colors.black.withValues(alpha: 0.14),
),
),
),
@ -335,48 +347,49 @@ class _MobileWorkspaceLauncher extends StatelessWidget {
@override
Widget build(BuildContext context) {
final connection = controller.connection;
final palette = context.palette;
final entries = <_WorkspaceEntry>[
_WorkspaceEntry(
destination: WorkspaceDestination.skills,
subtitle: appText('技能包与依赖状态', 'Packages and dependency status'),
iconColor: _blueLine,
iconBackground: _blueSoft,
iconColor: palette.accent,
iconBackground: palette.accentMuted,
),
_WorkspaceEntry(
destination: WorkspaceDestination.nodes,
subtitle: appText('边缘节点与实例', 'Edge nodes and instances'),
iconColor: const Color(0xFF5CC9B7),
iconBackground: const Color(0xFFDDF3EF),
iconColor: _tealLine,
iconBackground: _tealSoft,
),
_WorkspaceEntry(
destination: WorkspaceDestination.agents,
subtitle: appText('代理运行态与配置', 'Agent state and configuration'),
iconColor: _orangeLine,
iconBackground: _orangeSoft,
iconColor: palette.warning,
iconBackground: palette.warning.withValues(alpha: 0.12),
),
_WorkspaceEntry(
destination: WorkspaceDestination.mcpServer,
subtitle: appText('MCP 连接与工具注册', 'MCP endpoints and tools'),
iconColor: const Color(0xFF5E7CE2),
iconBackground: const Color(0xFFE1E8FB),
iconColor: palette.accent,
iconBackground: palette.accentMuted,
),
_WorkspaceEntry(
destination: WorkspaceDestination.clawHub,
subtitle: appText('技能与模板市场', 'Marketplace and templates'),
iconColor: const Color(0xFF845EC2),
iconBackground: const Color(0xFFECE2FF),
iconColor: _violetLine,
iconBackground: _violetSoft,
),
_WorkspaceEntry(
destination: WorkspaceDestination.aiGateway,
subtitle: appText('模型与代理网关', 'Models and agent gateway'),
iconColor: const Color(0xFF6B5CF2),
iconBackground: _accentSoft,
iconColor: palette.accent,
iconBackground: palette.accentMuted,
),
_WorkspaceEntry(
destination: WorkspaceDestination.account,
subtitle: appText('身份、工作区与会话', 'Identity, workspace and sessions'),
iconColor: _greenLine,
iconBackground: _greenSoft,
iconColor: palette.success,
iconBackground: palette.success.withValues(alpha: 0.12),
),
];
@ -469,21 +482,24 @@ class _LauncherHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final palette = context.palette;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 30,
fontWeight: FontWeight.w800,
color: _textPrimary,
style: theme.textTheme.displaySmall?.copyWith(
fontWeight: FontWeight.w700,
color: palette.textPrimary,
),
),
const SizedBox(height: 8),
Text(
subtitle,
style: const TextStyle(fontSize: 16, color: _textSecondary),
style: theme.textTheme.bodyLarge?.copyWith(
color: palette.textSecondary,
),
),
const SizedBox(height: 16),
Wrap(
@ -521,47 +537,54 @@ class _WorkspaceHero extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final palette = context.palette;
final statusLabel = connection.status == RuntimeConnectionStatus.connected
? appText('会话已就绪', 'Session Ready')
: appText('等待接入', 'Awaiting Connection');
final statusColor = connection.status == RuntimeConnectionStatus.connected
? _greenLine
: _textSecondary;
? palette.success
: palette.textSecondary;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: _surface,
borderRadius: BorderRadius.circular(28),
border: Border.all(color: _stroke, width: 1.2),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(alpha: 0.86),
palette.surfacePrimary.withValues(alpha: 0.94),
],
),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: palette.strokeSoft),
boxShadow: [palette.chromeShadowAmbient],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
statusLabel,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: statusColor,
),
style: theme.textTheme.labelLarge?.copyWith(color: statusColor),
),
const SizedBox(height: 10),
Text(
connection.remoteAddress ?? 'xworkmate.svc.plus',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.w800,
color: _textPrimary,
style: theme.textTheme.displaySmall?.copyWith(
fontWeight: FontWeight.w700,
color: palette.textPrimary,
),
),
const SizedBox(height: 8),
Text(
activeAgentName,
style: const TextStyle(fontSize: 16, color: _textSecondary),
style: theme.textTheme.bodyLarge?.copyWith(
color: palette.textSecondary,
),
),
const SizedBox(height: 18),
Wrap(
@ -604,23 +627,24 @@ class _HeroMetric extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final palette = context.palette;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: _surfaceSoft,
borderRadius: BorderRadius.circular(18),
color: palette.surfaceSecondary.withValues(alpha: 0.94),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: palette.strokeSoft),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18, color: _blueLine),
Icon(icon, size: 18, color: palette.accent),
const SizedBox(width: 8),
Text(
'$label · $value',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: _textPrimary,
style: theme.textTheme.labelLarge?.copyWith(
color: palette.textPrimary,
),
),
],
@ -637,17 +661,26 @@ class _WorkspaceShortcutCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final palette = context.palette;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(28),
borderRadius: BorderRadius.circular(24),
child: Ink(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: _surface,
borderRadius: BorderRadius.circular(28),
border: Border.all(color: _stroke, width: 1.2),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(alpha: 0.84),
palette.surfacePrimary.withValues(alpha: 0.94),
],
),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: palette.strokeSoft),
),
child: Row(
children: [
@ -671,10 +704,10 @@ class _WorkspaceShortcutCard extends StatelessWidget {
children: [
Text(
entry.destination.label,
style: const TextStyle(
style: theme.textTheme.headlineSmall?.copyWith(
fontSize: 18,
fontWeight: FontWeight.w800,
color: _textPrimary,
fontWeight: FontWeight.w700,
color: palette.textPrimary,
),
),
const SizedBox(height: 6),
@ -682,16 +715,15 @@ class _WorkspaceShortcutCard extends StatelessWidget {
entry.subtitle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14,
color: _textSecondary,
style: theme.textTheme.bodyMedium?.copyWith(
color: palette.textSecondary,
),
),
],
),
),
const SizedBox(width: 12),
const Icon(Icons.chevron_right_rounded, color: _textSecondary),
Icon(Icons.chevron_right_rounded, color: palette.textSecondary),
],
),
),
@ -708,10 +740,15 @@ class _GradientActionButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final palette = context.palette;
return DecoratedBox(
decoration: BoxDecoration(
gradient: const LinearGradient(colors: [_accentStart, _accentEnd]),
borderRadius: BorderRadius.circular(999),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [palette.accent, palette.accentHover],
),
borderRadius: BorderRadius.circular(14),
),
child: FilledButton(
onPressed: onPressed,
@ -719,6 +756,9 @@ class _GradientActionButton extends StatelessWidget {
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
),
child: Text(label),
@ -735,12 +775,14 @@ class _BottomPillNav extends StatelessWidget {
@override
Widget build(BuildContext context) {
final palette = context.palette;
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xF8FFFFFF),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: _stroke),
color: palette.surfacePrimary.withValues(alpha: 0.92),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: palette.strokeSoft),
boxShadow: [palette.chromeShadowAmbient],
),
child: Row(
children: MobileShellTab.values
@ -754,9 +796,9 @@ class _BottomPillNav extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: currentTab == tab
? _surfaceSoft
? palette.surfaceSecondary
: Colors.transparent,
borderRadius: BorderRadius.circular(999),
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
@ -764,7 +806,9 @@ class _BottomPillNav extends StatelessWidget {
Icon(
tab.icon,
size: 24,
color: currentTab == tab ? _blueLine : _textPrimary,
color: currentTab == tab
? palette.accent
: palette.textPrimary,
),
const SizedBox(height: 4),
Text(
@ -774,7 +818,9 @@ class _BottomPillNav extends StatelessWidget {
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: currentTab == tab ? _blueLine : _textPrimary,
color: currentTab == tab
? palette.accent
: palette.textPrimary,
),
),
],

View File

@ -63,85 +63,85 @@ class AppPalette extends ThemeExtension<AppPalette> {
final Color hover;
static const AppPalette light = AppPalette(
canvas: Color(0xFFF5F5F7),
sidebar: Color(0xFFF1F1F3),
sidebarBorder: Color(0xFFE5E5EA),
chromeBackground: Color(0xFFF6F4F1),
chromeSurface: Color(0xFFFCFBF9),
chromeSurfacePressed: Color(0xFFF2EFEB),
canvas: Color(0xFFF8F9FA),
sidebar: Color(0xFFF1F4F8),
sidebarBorder: Color(0x26A6B4C8),
chromeBackground: Color(0xFFF4F7FA),
chromeSurface: Color(0xFFFDFEFF),
chromeSurfacePressed: Color(0xFFF1F5F9),
chromeHighlight: Color(0xFFFFFFFF),
chromeStroke: Color(0xFFECE7E0),
chromeInset: Color(0xFFF4F1EC),
chromeStroke: Color(0x26A6B4C8),
chromeInset: Color(0xFFF4F7FA),
chromeShadowAmbient: BoxShadow(
color: Color(0x12000000),
blurRadius: 30,
offset: Offset(0, 10),
spreadRadius: -10,
color: Color(0x140058BD),
blurRadius: 40,
offset: Offset(0, 12),
spreadRadius: -14,
),
chromeShadowLift: BoxShadow(
color: Color(0x14000000),
blurRadius: 14,
offset: Offset(0, 5),
spreadRadius: -6,
color: Color(0x180058BD),
blurRadius: 24,
offset: Offset(0, 10),
spreadRadius: -12,
),
surfacePrimary: Color(0xFFFFFFFF),
surfaceSecondary: Color(0xFFF1F1F3),
surfaceTertiary: Color(0xFFECECEF),
stroke: Color(0xFFE5E5EA),
strokeSoft: Color(0xFFECECEF),
accent: Color(0xFF4C8BF5),
accentHover: Color(0xFF5E98F6),
accentMuted: Color(0xFFEEF4FF),
idle: Color(0xFFA1A1A6),
success: Color(0xFF34C759),
warning: Color(0xFFFF9F0A),
danger: Color(0xFFFF3B30),
textPrimary: Color(0xFF0A0A0A),
textSecondary: Color(0xFF6B6B6F),
textMuted: Color(0xFFA1A1A6),
shadow: Color(0x0A000000),
hover: Color(0xFFE5E5EA),
surfaceSecondary: Color(0xFFF2F5F8),
surfaceTertiary: Color(0xFFE9EEF4),
stroke: Color(0x33A6B4C8),
strokeSoft: Color(0x26A6B4C8),
accent: Color(0xFF0058BD),
accentHover: Color(0xFF1A6CCE),
accentMuted: Color(0xFFE8F0FB),
idle: Color(0xFF98A1B2),
success: Color(0xFF34A853),
warning: Color(0xFF8F4A00),
danger: Color(0xFFC3655C),
textPrimary: Color(0xFF1C1B1F),
textSecondary: Color(0xFF667085),
textMuted: Color(0xFF98A1B2),
shadow: Color(0x140058BD),
hover: Color(0xFFEFF4FA),
);
static const AppPalette dark = AppPalette(
canvas: Color(0xFF0E0F12),
sidebar: Color(0xFF15171C),
sidebarBorder: Color(0xFF23262D),
chromeBackground: Color(0xFF111318),
chromeSurface: Color(0xFF1A1D23),
chromeSurfacePressed: Color(0xFF16181D),
chromeHighlight: Color(0xFF2A2E36),
chromeStroke: Color(0xFF2A2F38),
chromeInset: Color(0xFF1E2229),
canvas: Color(0xFF141422),
sidebar: Color(0xFF1A1D2A),
sidebarBorder: Color(0x33CAC4D0),
chromeBackground: Color(0xFF161A26),
chromeSurface: Color(0xFF1D2230),
chromeSurfacePressed: Color(0xFF23293A),
chromeHighlight: Color(0xFF2A3145),
chromeStroke: Color(0x33CAC4D0),
chromeInset: Color(0xFF1A1F2C),
chromeShadowAmbient: BoxShadow(
color: Color(0x52000000),
blurRadius: 28,
offset: Offset(0, 10),
spreadRadius: -10,
color: Color(0x4D000814),
blurRadius: 36,
offset: Offset(0, 12),
spreadRadius: -14,
),
chromeShadowLift: BoxShadow(
color: Color(0x68000000),
blurRadius: 12,
offset: Offset(0, 4),
spreadRadius: -6,
color: Color(0x660058BD),
blurRadius: 22,
offset: Offset(0, 8),
spreadRadius: -12,
),
surfacePrimary: Color(0xFF15171C),
surfaceSecondary: Color(0xFF1B1E24),
surfaceTertiary: Color(0xFF22262E),
stroke: Color(0xFF2B3038),
strokeSoft: Color(0xFF22262E),
accent: Color(0xFF4C8BF5),
accentHover: Color(0xFF6A9DF7),
accentMuted: Color(0xFF1A2740),
idle: Color(0xFFA1A1AA),
success: Color(0xFF34C759),
warning: Color(0xFFFF9F0A),
danger: Color(0xFFFF3B30),
textPrimary: Color(0xFFFFFFFF),
textSecondary: Color(0xFFA1A1AA),
textMuted: Color(0xFF737982),
surfacePrimary: Color(0xFF171C28),
surfaceSecondary: Color(0xFF1E2433),
surfaceTertiary: Color(0xFF262D3F),
stroke: Color(0x40CAC4D0),
strokeSoft: Color(0x26CAC4D0),
accent: Color(0xFF4B8FE8),
accentHover: Color(0xFF78AFFF),
accentMuted: Color(0xFF1C3355),
idle: Color(0xFF8B95A8),
success: Color(0xFF5CB978),
warning: Color(0xFFE0AE5A),
danger: Color(0xFFEF9A9A),
textPrimary: Color(0xFFE6E1E5),
textSecondary: Color(0xFFB0B8C8),
textMuted: Color(0xFF8B95A8),
shadow: Color(0x52000000),
hover: Color(0xFF1B1E24),
hover: Color(0xFF23293A),
);
@override

View File

@ -21,14 +21,14 @@ class SimpleSpacing {
class SimpleRadius {
SimpleRadius._();
static const double card = 6.0;
static const double button = 8.0;
static const double input = 8.0;
static const double chip = 8.0;
static const double card = 16.0;
static const double button = 12.0;
static const double input = 14.0;
static const double chip = 12.0;
static const double badge = 999.0;
static const double dialog = 5.0;
static const double sidebar = 8.0;
static const double icon = 8.0;
static const double dialog = 18.0;
static const double sidebar = 20.0;
static const double icon = 12.0;
}
class SimpleTypography {
@ -66,18 +66,18 @@ class SimpleTypography {
class SimpleSizes {
SimpleSizes._();
static const double sidebarItemHeight = 32.0;
static const double sidebarItemHeight = 34.0;
static const double sidebarIconSize = 18.0;
static const double sidebarTextSize = 13.0;
static const double sidebarExpandedWidth = 176.0;
static const double sidebarCollapsedWidth = 52.0;
static const double sidebarExpandedWidth = 188.0;
static const double sidebarCollapsedWidth = 56.0;
static const double textareaHeight = 36.0;
static const double toolbarHeight = 40.0;
static const double inputHeight = 36.0;
static const double buttonHeightDesktop = 16.0;
static const double buttonHeightMobile = 16.0;
static const double inputHeight = 40.0;
static const double buttonHeightDesktop = 30.0;
static const double buttonHeightMobile = 36.0;
}
class AppSpacing {
@ -244,37 +244,45 @@ class AppTheme {
selectedColor: palette.surfacePrimary,
secondarySelectedColor: palette.surfacePrimary,
disabledColor: palette.surfaceSecondary,
side: BorderSide.none,
side: BorderSide(color: palette.strokeSoft),
checkmarkColor: Colors.transparent,
labelStyle: tunedTextTheme.labelMedium,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.chip),
),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: palette.accent,
foregroundColor: Colors.white,
shadowColor: palette.shadow,
elevation: 0,
surfaceTintColor: Colors.transparent,
textStyle: tunedTextTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
style: ButtonStyle(
foregroundColor: const WidgetStatePropertyAll(Colors.white),
shadowColor: WidgetStatePropertyAll(palette.shadow),
elevation: const WidgetStatePropertyAll(0),
surfaceTintColor: const WidgetStatePropertyAll(Colors.transparent),
textStyle: WidgetStatePropertyAll(
tunedTextTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600),
),
minimumSize: Size(
0,
isDesktop
? AppSizes.buttonHeightDesktop
: AppSizes.buttonHeightMobile,
minimumSize: WidgetStatePropertyAll(
Size(
0,
isDesktop
? AppSizes.buttonHeightDesktop
: AppSizes.buttonHeightMobile,
),
),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: 0,
padding: const WidgetStatePropertyAll(
EdgeInsets.symmetric(horizontal: 12, vertical: 0),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
),
),
backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.pressed)) {
return palette.accentHover;
}
return palette.accent;
}),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
@ -293,10 +301,7 @@ class AppTheme {
? AppSizes.buttonHeightDesktop
: AppSizes.buttonHeightMobile,
),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: 0,
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
),
@ -316,7 +321,7 @@ class AppTheme {
: AppSizes.buttonHeightMobile,
),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.xs,
horizontal: AppSpacing.sm,
vertical: 0,
),
shape: RoundedRectangleBorder(
@ -327,10 +332,10 @@ class AppTheme {
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(
foregroundColor: palette.textSecondary,
backgroundColor: palette.surfaceSecondary,
backgroundColor: palette.surfaceSecondary.withValues(alpha: 0.88),
surfaceTintColor: Colors.transparent,
minimumSize: const Size(40, 40),
padding: const EdgeInsets.all(10),
minimumSize: const Size(32, 32),
padding: const EdgeInsets.all(7),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.icon),
),
@ -339,7 +344,7 @@ class AppTheme {
inputDecorationTheme: InputDecorationTheme(
isDense: true,
filled: true,
fillColor: palette.surfacePrimary,
fillColor: palette.surfacePrimary.withValues(alpha: 0.92),
hintStyle: tunedTextTheme.bodyMedium?.copyWith(
color: palette.textMuted,
),
@ -364,7 +369,7 @@ class AppTheme {
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: BorderSide(color: palette.accent.withValues(alpha: 0.18)),
borderSide: BorderSide(color: palette.accent.withValues(alpha: 0.32)),
),
),
segmentedButtonTheme: SegmentedButtonThemeData(
@ -372,7 +377,7 @@ class AppTheme {
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return palette.surfacePrimary;
return palette.surfacePrimary.withValues(alpha: 0.96);
}
return palette.surfaceSecondary;
}),
@ -400,14 +405,14 @@ class AppTheme {
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
backgroundColor: palette.surfacePrimary,
backgroundColor: palette.surfacePrimary.withValues(alpha: 0.96),
contentTextStyle: TextStyle(color: palette.textPrimary),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.dialog),
),
),
popupMenuTheme: PopupMenuThemeData(
color: palette.surfacePrimary,
color: palette.surfacePrimary.withValues(alpha: 0.98),
surfaceTintColor: Colors.transparent,
elevation: 0,
shape: RoundedRectangleBorder(
@ -449,7 +454,7 @@ class AppTheme {
base.titleLarge?.copyWith(
fontSize: AppTypography.sectionSize,
fontWeight: FontWeight.w600,
letterSpacing: 0,
letterSpacing: 0.02,
height: AppTypography.sectionHeight,
color: palette.textPrimary,
),
@ -506,7 +511,8 @@ class AppTheme {
labelMedium: withUiFont(
base.labelMedium?.copyWith(
fontSize: AppTypography.captionSize,
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w600,
letterSpacing: 0.2,
height: AppTypography.captionHeight,
color: palette.textSecondary,
),

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../theme/app_palette.dart';
import '../theme/app_theme.dart';
class DesktopWorkspaceScaffold extends StatelessWidget {
const DesktopWorkspaceScaffold({
@ -48,7 +49,7 @@ class DesktopWorkspaceScaffold extends StatelessWidget {
style: Theme.of(context).textTheme.labelMedium
?.copyWith(
color: palette.textMuted,
letterSpacing: 0.2,
letterSpacing: 0.32,
),
),
const SizedBox(height: 6),
@ -101,16 +102,16 @@ class DesktopWorkspaceScaffold extends StatelessWidget {
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(alpha: 0.96),
palette.chromeSurface,
palette.chromeHighlight.withValues(alpha: 0.9),
palette.chromeSurface.withValues(alpha: 0.92),
],
),
borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(AppRadius.card),
border: Border.all(color: palette.chromeStroke),
boxShadow: [palette.chromeShadowAmbient],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(AppRadius.card),
child: child,
),
),

View File

@ -85,8 +85,8 @@ class SidebarNavigation extends StatelessWidget {
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(alpha: 0.96),
palette.chromeSurface,
palette.chromeHighlight.withValues(alpha: 0.9),
palette.chromeSurface.withValues(alpha: 0.92),
],
),
borderRadius: BorderRadius.circular(AppRadius.sidebar),
@ -196,8 +196,8 @@ class SidebarHeader extends StatelessWidget {
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(alpha: 0.94),
palette.chromeSurfacePressed,
palette.chromeHighlight.withValues(alpha: 0.88),
palette.chromeSurfacePressed.withValues(alpha: 0.92),
],
),
border: Border.all(color: palette.chromeStroke),
@ -261,6 +261,7 @@ class _SidebarSectionGroup extends StatelessWidget {
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: palette.textMuted,
fontWeight: FontWeight.w600,
letterSpacing: 0.28,
),
),
),
@ -326,7 +327,7 @@ class _SidebarNavItemState extends State<_SidebarNavItem> {
final theme = Theme.of(context);
final isPrimary = widget.emphasis == _SidebarItemEmphasis.primary;
final background = widget.selected
? palette.chromeSurface
? palette.surfacePrimary
: _hovered
? palette.chromeSurfacePressed
: Colors.transparent;
@ -350,9 +351,11 @@ class _SidebarNavItemState extends State<_SidebarNavItem> {
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(
alpha: widget.selected ? 0.96 : 0.82,
alpha: widget.selected ? 0.84 : 0.7,
),
background.withValues(
alpha: widget.selected ? 0.96 : 0.9,
),
background,
],
)
: null,
@ -372,7 +375,7 @@ class _SidebarNavItemState extends State<_SidebarNavItem> {
onTap: widget.onTap,
child: Container(
height: height,
padding: const EdgeInsets.symmetric(horizontal: 6),
padding: const EdgeInsets.symmetric(horizontal: 8),
child: widget.collapsed
? Center(
child: Icon(
@ -411,6 +414,7 @@ class _SidebarNavItemState extends State<_SidebarNavItem> {
fontWeight: isPrimary
? FontWeight.w600
: FontWeight.w500,
letterSpacing: isPrimary ? 0.02 : 0,
),
),
),

View File

@ -47,17 +47,22 @@ class _SurfaceCardState extends State<SurfaceCard> {
};
final decoration = switch (widget.tone) {
SurfaceCardTone.standard => BoxDecoration(
color: _hovered && widget.onTap != null ? hoveredColor : baseColor,
color: (_hovered && widget.onTap != null ? hoveredColor : baseColor)
.withValues(alpha: 0.94),
border: Border.all(color: borderColor),
borderRadius: BorderRadius.circular(widget.borderRadius),
boxShadow: widget.onTap != null && _hovered
? [palette.chromeShadowLift]
: const [],
),
SurfaceCardTone.chrome => BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(alpha: 0.94),
_hovered && widget.onTap != null ? hoveredColor : baseColor,
palette.chromeHighlight.withValues(alpha: 0.92),
(_hovered && widget.onTap != null ? hoveredColor : baseColor)
.withValues(alpha: 0.9),
],
),
border: Border.all(color: borderColor),
@ -74,7 +79,7 @@ class _SurfaceCardState extends State<SurfaceCard> {
onExit: (_) => setState(() => _hovered = false),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
curve: Curves.easeInOut,
curve: Curves.easeOutCubic,
decoration: decoration,
child: Material(
color: Colors.transparent,

View File

@ -71,11 +71,7 @@ class TopBar extends StatelessWidget {
}
class AppBreadcrumbItem {
const AppBreadcrumbItem({
required this.label,
this.onTap,
this.icon,
});
const AppBreadcrumbItem({required this.label, this.onTap, this.icon});
final String label;
final VoidCallback? onTap;
@ -149,10 +145,12 @@ class _BreadcrumbChip extends StatelessWidget {
final body = Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: item.onTap != null
? palette.surfaceSecondary
: palette.surfacePrimary,
borderRadius: BorderRadius.circular(999),
color:
(item.onTap != null
? palette.surfaceSecondary
: palette.surfacePrimary)
.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: palette.strokeSoft),
),
child: content,