soften desktop chrome surfaces

This commit is contained in:
Haitao Pan 2026-03-18 19:58:31 +08:00
parent 0c463f4492
commit 9f53270746
8 changed files with 546 additions and 124 deletions

View File

@ -69,6 +69,7 @@ class _AppShellState extends State<AppShell> {
builder: (context, constraints) {
final palette = context.palette;
final platform = Theme.of(context).platform;
final brightness = Theme.of(context).brightness;
final isCompactMobile =
(platform == TargetPlatform.iOS ||
platform == TargetPlatform.android) &&
@ -272,10 +273,71 @@ class _AppShellState extends State<AppShell> {
),
child: DecoratedBox(
decoration: BoxDecoration(
color: palette.canvas,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeBackground,
palette.canvas,
],
),
),
child: Stack(
children: [
Positioned(
top: -180,
right: -80,
child: IgnorePointer(
child: Container(
width: 420,
height: 420,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
palette.chromeHighlight
.withValues(
alpha:
brightness ==
Brightness.dark
? 0.10
: 0.58,
),
palette.chromeHighlight
.withValues(alpha: 0),
],
),
),
),
),
),
Positioned(
bottom: -220,
left: -140,
child: IgnorePointer(
child: Container(
width: 360,
height: 360,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
palette.chromeInset.withValues(
alpha:
brightness ==
Brightness.dark
? 0.10
: 0.36,
),
palette.chromeInset.withValues(
alpha: 0,
),
],
),
),
),
),
),
_buildCurrentPage(controller.openDetail),
],
),
@ -419,13 +481,24 @@ class _SidebarRevealRailState extends State<_SidebarRevealRail> {
duration: const Duration(milliseconds: 180),
width: _hovered ? 22 : 10,
decoration: BoxDecoration(
color: _hovered ? palette.surfaceSecondary : Colors.transparent,
gradient: _hovered
? LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(alpha: 0.92),
palette.chromeSurface,
],
)
: null,
color: _hovered ? null : Colors.transparent,
borderRadius: const BorderRadius.horizontal(
right: Radius.circular(14),
),
border: Border.all(
color: _hovered ? palette.strokeSoft : Colors.transparent,
color: _hovered ? palette.chromeStroke : Colors.transparent,
),
boxShadow: _hovered ? [palette.chromeShadowLift] : const [],
),
child: _hovered
? Icon(

View File

@ -1086,9 +1086,17 @@ class _AssistantSideTabRail extends StatelessWidget {
key: const Key('assistant-side-pane'),
width: 46,
decoration: BoxDecoration(
color: palette.sidebar,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(alpha: 0.96),
palette.chromeSurface,
],
),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: palette.sidebarBorder),
border: Border.all(color: palette.chromeStroke),
boxShadow: [palette.chromeShadowAmbient],
),
child: Column(
children: [
@ -1110,7 +1118,7 @@ class _AssistantSideTabRail extends StatelessWidget {
),
if (favoriteDestinations.isNotEmpty) ...[
const SizedBox(height: 4),
Container(width: 24, height: 1, color: palette.strokeSoft),
Container(width: 24, height: 1, color: palette.chromeStroke),
const SizedBox(height: 4),
Expanded(
child: SingleChildScrollView(
@ -1146,6 +1154,14 @@ class _AssistantSideTabRail extends StatelessWidget {
? appText('展开侧板', 'Expand side pane')
: appText('收起侧板', 'Collapse side pane'),
onPressed: onToggleCollapsed,
style: IconButton.styleFrom(
backgroundColor: palette.chromeSurface,
foregroundColor: palette.textSecondary,
side: BorderSide(color: palette.chromeStroke),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
icon: Icon(
collapsed
? Icons.keyboard_double_arrow_right_rounded
@ -1160,7 +1176,7 @@ class _AssistantSideTabRail extends StatelessWidget {
}
}
class _AssistantSideTabButton extends StatelessWidget {
class _AssistantSideTabButton extends StatefulWidget {
const _AssistantSideTabButton({
super.key,
required this.icon,
@ -1174,31 +1190,64 @@ class _AssistantSideTabButton extends StatelessWidget {
final String tooltip;
final VoidCallback onTap;
@override
State<_AssistantSideTabButton> createState() =>
_AssistantSideTabButtonState();
}
class _AssistantSideTabButtonState extends State<_AssistantSideTabButton> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final palette = context.palette;
return Tooltip(
message: tooltip,
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: onTap,
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: selected ? palette.surfacePrimary : Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: selected ? palette.strokeSoft : Colors.transparent,
message: widget.tooltip,
child: MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: widget.onTap,
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
gradient: widget.selected || _hovered
? LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(
alpha: widget.selected ? 0.96 : 0.84,
),
widget.selected
? palette.chromeSurface
: palette.chromeSurfacePressed,
],
)
: null,
color: widget.selected || _hovered ? null : Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: widget.selected || _hovered
? palette.chromeStroke
: Colors.transparent,
),
boxShadow: widget.selected
? [palette.chromeShadowLift]
: const [],
),
child: Icon(
widget.icon,
size: 18,
color: widget.selected
? palette.textPrimary
: palette.textSecondary,
),
),
child: Icon(
icon,
size: 18,
color: selected ? palette.textPrimary : palette.textSecondary,
),
),
),
@ -1306,6 +1355,7 @@ class _ConversationArea extends StatelessWidget {
return SurfaceCard(
borderRadius: 0,
padding: EdgeInsets.zero,
tone: SurfaceCardTone.chrome,
child: Column(
children: [
Padding(
@ -1492,6 +1542,7 @@ class _AssistantTaskRail extends StatelessWidget {
return SurfaceCard(
borderRadius: 0,
padding: EdgeInsets.zero,
tone: SurfaceCardTone.chrome,
child: Column(
children: [
Padding(
@ -1970,6 +2021,7 @@ class _ComposerBar extends StatelessWidget {
return SurfaceCard(
borderRadius: 10,
tone: SurfaceCardTone.chrome,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -2060,11 +2112,11 @@ class _ComposerBar extends StatelessWidget {
decoration: InputDecoration(
isCollapsed: true,
filled: true,
fillColor: palette.surfacePrimary,
fillColor: palette.chromeSurface,
contentPadding: const EdgeInsets.fromLTRB(10, 8, 10, 8),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: palette.strokeSoft),
borderSide: BorderSide(color: palette.chromeStroke),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
@ -2376,27 +2428,50 @@ class _ComposerBar extends StatelessWidget {
}
}
class _ComposerIconButton extends StatelessWidget {
class _ComposerIconButton extends StatefulWidget {
const _ComposerIconButton({required this.icon});
final IconData icon;
@override
State<_ComposerIconButton> createState() => _ComposerIconButtonState();
}
class _ComposerIconButtonState extends State<_ComposerIconButton> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
return Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: context.palette.surfaceSecondary,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: context.palette.strokeSoft),
final palette = context.palette;
return MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(alpha: _hovered ? 0.94 : 0.88),
_hovered ? palette.chromeSurfacePressed : palette.chromeSurface,
],
),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: palette.chromeStroke),
boxShadow: [
_hovered ? palette.chromeShadowLift : palette.chromeShadowAmbient,
],
),
child: Icon(widget.icon, size: 18, color: palette.textMuted),
),
child: Icon(icon, size: 18, color: context.palette.textMuted),
);
}
}
class _ComposerToolbarChip extends StatelessWidget {
class _ComposerToolbarChip extends StatefulWidget {
const _ComposerToolbarChip({
super.key,
required this.icon,
@ -2415,43 +2490,64 @@ class _ComposerToolbarChip extends StatelessWidget {
final double maxLabelWidth;
final EdgeInsetsGeometry padding;
@override
State<_ComposerToolbarChip> createState() => _ComposerToolbarChipState();
}
class _ComposerToolbarChipState extends State<_ComposerToolbarChip> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final palette = context.palette;
return Container(
padding: padding,
decoration: BoxDecoration(
color: palette.surfaceSecondary,
borderRadius: BorderRadius.circular(AppRadius.chip),
border: Border.all(color: palette.strokeSoft),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 13, color: palette.textMuted),
const SizedBox(width: 4),
ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxLabelWidth),
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onSurface,
return MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: Container(
padding: widget.padding,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(alpha: _hovered ? 0.94 : 0.88),
_hovered ? palette.chromeSurfacePressed : palette.chromeSurface,
],
),
borderRadius: BorderRadius.circular(AppRadius.chip),
border: Border.all(color: palette.chromeStroke),
boxShadow: [
_hovered ? palette.chromeShadowLift : palette.chromeShadowAmbient,
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(widget.icon, size: 13, color: palette.textMuted),
const SizedBox(width: 4),
ConstrainedBox(
constraints: BoxConstraints(maxWidth: widget.maxLabelWidth),
child: Text(
widget.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onSurface,
),
),
),
),
if (showChevron) ...[
const SizedBox(width: 2),
Icon(
Icons.keyboard_arrow_down_rounded,
size: 14,
color: palette.textMuted,
),
if (widget.showChevron) ...[
const SizedBox(width: 2),
Icon(
Icons.keyboard_arrow_down_rounded,
size: 14,
color: palette.textMuted,
),
],
],
],
),
),
);
}

View File

@ -6,6 +6,14 @@ class AppPalette extends ThemeExtension<AppPalette> {
required this.canvas,
required this.sidebar,
required this.sidebarBorder,
required this.chromeBackground,
required this.chromeSurface,
required this.chromeSurfacePressed,
required this.chromeHighlight,
required this.chromeStroke,
required this.chromeInset,
required this.chromeShadowAmbient,
required this.chromeShadowLift,
required this.surfacePrimary,
required this.surfaceSecondary,
required this.surfaceTertiary,
@ -28,6 +36,14 @@ class AppPalette extends ThemeExtension<AppPalette> {
final Color canvas;
final Color sidebar;
final Color sidebarBorder;
final Color chromeBackground;
final Color chromeSurface;
final Color chromeSurfacePressed;
final Color chromeHighlight;
final Color chromeStroke;
final Color chromeInset;
final BoxShadow chromeShadowAmbient;
final BoxShadow chromeShadowLift;
final Color surfacePrimary;
final Color surfaceSecondary;
final Color surfaceTertiary;
@ -50,6 +66,24 @@ class AppPalette extends ThemeExtension<AppPalette> {
canvas: Color(0xFFF5F5F7),
sidebar: Color(0xFFF1F1F3),
sidebarBorder: Color(0xFFE5E5EA),
chromeBackground: Color(0xFFF6F4F1),
chromeSurface: Color(0xFFFCFBF9),
chromeSurfacePressed: Color(0xFFF2EFEB),
chromeHighlight: Color(0xFFFFFFFF),
chromeStroke: Color(0xFFECE7E0),
chromeInset: Color(0xFFF4F1EC),
chromeShadowAmbient: BoxShadow(
color: Color(0x12000000),
blurRadius: 30,
offset: Offset(0, 10),
spreadRadius: -10,
),
chromeShadowLift: BoxShadow(
color: Color(0x14000000),
blurRadius: 14,
offset: Offset(0, 5),
spreadRadius: -6,
),
surfacePrimary: Color(0xFFFFFFFF),
surfaceSecondary: Color(0xFFF1F1F3),
surfaceTertiary: Color(0xFFECECEF),
@ -73,6 +107,24 @@ class AppPalette extends ThemeExtension<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),
chromeShadowAmbient: BoxShadow(
color: Color(0x52000000),
blurRadius: 28,
offset: Offset(0, 10),
spreadRadius: -10,
),
chromeShadowLift: BoxShadow(
color: Color(0x68000000),
blurRadius: 12,
offset: Offset(0, 4),
spreadRadius: -6,
),
surfacePrimary: Color(0xFF15171C),
surfaceSecondary: Color(0xFF1B1E24),
surfaceTertiary: Color(0xFF22262E),
@ -97,6 +149,14 @@ class AppPalette extends ThemeExtension<AppPalette> {
Color? canvas,
Color? sidebar,
Color? sidebarBorder,
Color? chromeBackground,
Color? chromeSurface,
Color? chromeSurfacePressed,
Color? chromeHighlight,
Color? chromeStroke,
Color? chromeInset,
BoxShadow? chromeShadowAmbient,
BoxShadow? chromeShadowLift,
Color? surfacePrimary,
Color? surfaceSecondary,
Color? surfaceTertiary,
@ -119,6 +179,14 @@ class AppPalette extends ThemeExtension<AppPalette> {
canvas: canvas ?? this.canvas,
sidebar: sidebar ?? this.sidebar,
sidebarBorder: sidebarBorder ?? this.sidebarBorder,
chromeBackground: chromeBackground ?? this.chromeBackground,
chromeSurface: chromeSurface ?? this.chromeSurface,
chromeSurfacePressed: chromeSurfacePressed ?? this.chromeSurfacePressed,
chromeHighlight: chromeHighlight ?? this.chromeHighlight,
chromeStroke: chromeStroke ?? this.chromeStroke,
chromeInset: chromeInset ?? this.chromeInset,
chromeShadowAmbient: chromeShadowAmbient ?? this.chromeShadowAmbient,
chromeShadowLift: chromeShadowLift ?? this.chromeShadowLift,
surfacePrimary: surfacePrimary ?? this.surfacePrimary,
surfaceSecondary: surfaceSecondary ?? this.surfaceSecondary,
surfaceTertiary: surfaceTertiary ?? this.surfaceTertiary,
@ -153,6 +221,26 @@ class AppPalette extends ThemeExtension<AppPalette> {
sidebar: Color.lerp(sidebar, other.sidebar, t) ?? sidebar,
sidebarBorder:
Color.lerp(sidebarBorder, other.sidebarBorder, t) ?? sidebarBorder,
chromeBackground:
Color.lerp(chromeBackground, other.chromeBackground, t) ??
chromeBackground,
chromeSurface:
Color.lerp(chromeSurface, other.chromeSurface, t) ?? chromeSurface,
chromeSurfacePressed:
Color.lerp(chromeSurfacePressed, other.chromeSurfacePressed, t) ??
chromeSurfacePressed,
chromeHighlight:
Color.lerp(chromeHighlight, other.chromeHighlight, t) ??
chromeHighlight,
chromeStroke:
Color.lerp(chromeStroke, other.chromeStroke, t) ?? chromeStroke,
chromeInset: Color.lerp(chromeInset, other.chromeInset, t) ?? chromeInset,
chromeShadowAmbient:
BoxShadow.lerp(chromeShadowAmbient, other.chromeShadowAmbient, t) ??
chromeShadowAmbient,
chromeShadowLift:
BoxShadow.lerp(chromeShadowLift, other.chromeShadowLift, t) ??
chromeShadowLift,
surfacePrimary:
Color.lerp(surfacePrimary, other.surfacePrimary, t) ?? surfacePrimary,
surfaceSecondary:

View File

@ -54,6 +54,7 @@ class _AssistantFocusPanelState extends State<AssistantFocusPanel> {
return SurfaceCard(
borderRadius: 16,
padding: EdgeInsets.zero,
tone: SurfaceCardTone.chrome,
child: Column(
children: [
Padding(
@ -109,9 +110,17 @@ class _AssistantFocusPanelState extends State<AssistantFocusPanel> {
width: 38,
height: 38,
decoration: BoxDecoration(
color: palette.surfaceSecondary,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(alpha: 0.94),
palette.chromeSurfacePressed,
],
),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: palette.strokeSoft),
border: Border.all(color: palette.chromeStroke),
boxShadow: [palette.chromeShadowLift],
),
child: Icon(
Icons.add_rounded,
@ -289,10 +298,16 @@ class _AssistantFocusPreview extends StatelessWidget {
Widget build(BuildContext context) {
return switch (destination) {
WorkspaceDestination.tasks => _TasksFocusPreview(controller: controller),
WorkspaceDestination.skills => _SkillsFocusPreview(controller: controller),
WorkspaceDestination.skills => _SkillsFocusPreview(
controller: controller,
),
WorkspaceDestination.nodes => _NodesFocusPreview(controller: controller),
WorkspaceDestination.agents => _AgentsFocusPreview(controller: controller),
WorkspaceDestination.mcpServer => _McpFocusPreview(controller: controller),
WorkspaceDestination.agents => _AgentsFocusPreview(
controller: controller,
),
WorkspaceDestination.mcpServer => _McpFocusPreview(
controller: controller,
),
WorkspaceDestination.clawHub => _ClawHubFocusPreview(
controller: controller,
),
@ -353,10 +368,14 @@ class _TasksFocusPreview extends StatelessWidget {
const SizedBox(height: 12),
if (items.isEmpty)
_PreviewEmptyState(
message: controller.connection.status ==
message:
controller.connection.status ==
RuntimeConnectionStatus.connected
? appText('当前没有任务摘要。', 'No task summary yet.')
: appText('连接 Gateway 后这里会显示任务摘要。', 'Connect a gateway to load task summaries.'),
: appText(
'连接 Gateway 后这里会显示任务摘要。',
'Connect a gateway to load task summaries.',
),
)
else
...items.map(
@ -384,9 +403,16 @@ class _SkillsFocusPreview extends StatelessWidget {
final items = controller.skills.take(4).toList(growable: false);
if (items.isEmpty) {
return _PreviewEmptyState(
message: controller.connection.status == RuntimeConnectionStatus.connected
? appText('当前代理没有已加载技能。', 'No skills are loaded for the active agent.')
: appText('连接 Gateway 后可查看技能摘要。', 'Connect a gateway to inspect skills here.'),
message:
controller.connection.status == RuntimeConnectionStatus.connected
? appText(
'当前代理没有已加载技能。',
'No skills are loaded for the active agent.',
)
: appText(
'连接 Gateway 后可查看技能摘要。',
'Connect a gateway to inspect skills here.',
),
);
}
return Column(
@ -430,11 +456,11 @@ class _NodesFocusPreview extends StatelessWidget {
title: instance.host?.trim().isNotEmpty == true
? instance.host!
: instance.id,
subtitle: [
instance.platform,
instance.deviceFamily,
instance.ip,
].whereType<String>().where((item) => item.trim().isNotEmpty).join(' · '),
subtitle:
[instance.platform, instance.deviceFamily, instance.ip]
.whereType<String>()
.where((item) => item.trim().isNotEmpty)
.join(' · '),
trailing: instance.mode ?? appText('未知', 'Unknown'),
),
),
@ -662,13 +688,19 @@ class _SettingsFocusPreview extends StatelessWidget {
const SizedBox(height: 8),
_FocusListTile(
title: appText('执行目标', 'Execution target'),
subtitle: appText('Assistant 默认运行位置', 'Default assistant execution target'),
subtitle: appText(
'Assistant 默认运行位置',
'Default assistant execution target',
),
trailing: controller.assistantExecutionTarget.label,
),
const SizedBox(height: 8),
_FocusListTile(
title: appText('权限', 'Permissions'),
subtitle: appText('Assistant 默认权限级别', 'Default assistant permission level'),
subtitle: appText(
'Assistant 默认权限级别',
'Default assistant permission level',
),
trailing: controller.assistantPermissionLevel.label,
),
],

View File

@ -97,9 +97,17 @@ class DesktopWorkspaceScaffold extends StatelessWidget {
Expanded(
child: DecoratedBox(
decoration: BoxDecoration(
color: palette.surfacePrimary,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(alpha: 0.96),
palette.chromeSurface,
],
),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: palette.strokeSoft),
border: Border.all(color: palette.chromeStroke),
boxShadow: [palette.chromeShadowAmbient],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),

View File

@ -36,9 +36,17 @@ class SectionTabs extends StatelessWidget {
return Container(
padding: const EdgeInsets.all(AppSpacing.xxs),
decoration: BoxDecoration(
color: palette.surfaceSecondary,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(alpha: 0.94),
palette.chromeSurfacePressed,
],
),
borderRadius: BorderRadius.circular(AppRadius.chip),
border: Border.all(color: palette.strokeSoft),
border: Border.all(color: palette.chromeStroke),
boxShadow: [palette.chromeShadowAmbient],
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
@ -92,15 +100,28 @@ class _SectionTabChipState extends State<_SectionTabChip> {
duration: const Duration(milliseconds: 160),
curve: Curves.easeInOut,
decoration: BoxDecoration(
color: widget.selected
? palette.surfacePrimary
: _hovered
? palette.surfaceTertiary
: Colors.transparent,
gradient: widget.selected || _hovered
? LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(
alpha: widget.selected ? 0.96 : 0.86,
),
widget.selected
? palette.chromeSurface
: palette.chromeSurfacePressed,
],
)
: null,
color: widget.selected || _hovered ? null : Colors.transparent,
borderRadius: BorderRadius.circular(AppRadius.button),
border: Border.all(
color: widget.selected ? palette.stroke : Colors.transparent,
color: widget.selected || _hovered
? palette.chromeStroke
: Colors.transparent,
),
boxShadow: widget.selected ? [palette.chromeShadowLift] : const [],
),
child: Material(
color: Colors.transparent,

View File

@ -81,9 +81,17 @@ class SidebarNavigation extends StatelessWidget {
height: double.infinity,
margin: marginOverride ?? const EdgeInsets.fromLTRB(4, 4, 4, 0),
decoration: BoxDecoration(
color: palette.sidebar,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(alpha: 0.96),
palette.chromeSurface,
],
),
borderRadius: BorderRadius.circular(AppRadius.sidebar),
border: Border.all(color: palette.sidebarBorder),
border: Border.all(color: palette.chromeStroke),
boxShadow: [palette.chromeShadowAmbient],
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
@ -184,8 +192,16 @@ class SidebarHeader extends StatelessWidget {
height: isCollapsed ? 36 : 28,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: palette.surfaceSecondary,
border: Border.all(color: palette.strokeSoft),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(alpha: 0.94),
palette.chromeSurfacePressed,
],
),
border: Border.all(color: palette.chromeStroke),
boxShadow: [palette.chromeShadowLift],
),
child: Icon(
Icons.crop_square_rounded,
@ -310,9 +326,9 @@ class _SidebarNavItemState extends State<_SidebarNavItem> {
final theme = Theme.of(context);
final isPrimary = widget.emphasis == _SidebarItemEmphasis.primary;
final background = widget.selected
? palette.surfacePrimary
? palette.chromeSurface
: _hovered
? palette.surfaceTertiary
? palette.chromeSurfacePressed
: Colors.transparent;
final iconColor = widget.selected
? palette.textPrimary
@ -328,11 +344,26 @@ class _SidebarNavItemState extends State<_SidebarNavItem> {
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
decoration: BoxDecoration(
color: background,
gradient: widget.selected || _hovered
? LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(
alpha: widget.selected ? 0.96 : 0.82,
),
background,
],
)
: null,
color: widget.selected || _hovered ? null : Colors.transparent,
borderRadius: BorderRadius.circular(radius),
border: Border.all(
color: widget.selected ? palette.strokeSoft : Colors.transparent,
color: widget.selected || _hovered
? palette.chromeStroke
: Colors.transparent,
),
boxShadow: widget.selected ? [palette.chromeShadowLift] : const [],
),
child: Material(
color: Colors.transparent,
@ -509,7 +540,7 @@ class SidebarFooter extends StatelessWidget {
children: [
Container(
height: 1,
color: palette.sidebarBorder.withValues(alpha: 0.7),
color: palette.chromeStroke.withValues(alpha: 0.9),
),
const SizedBox(height: 6),
_SidebarLanguageButton(
@ -569,7 +600,7 @@ class SidebarFooter extends StatelessWidget {
children: [
Container(
height: 1,
color: palette.sidebarBorder.withValues(alpha: 0.7),
color: palette.chromeStroke.withValues(alpha: 0.9),
),
const SizedBox(height: AppSpacing.xs),
_SidebarNavItem(
@ -665,8 +696,8 @@ class _SidebarActionButtonState extends State<_SidebarActionButton> {
Widget build(BuildContext context) {
final palette = context.palette;
final resolvedBackground = _hovered
? palette.surfaceTertiary
: palette.surfaceSecondary;
? palette.chromeSurfacePressed
: palette.chromeSurface;
return Tooltip(
message: widget.tooltip ?? '',
@ -676,9 +707,21 @@ class _SidebarActionButtonState extends State<_SidebarActionButton> {
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
decoration: BoxDecoration(
color: resolvedBackground,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(
alpha: _hovered ? 0.94 : 0.88,
),
resolvedBackground,
],
),
borderRadius: BorderRadius.circular(AppRadius.button),
border: Border.all(color: palette.strokeSoft),
border: Border.all(color: palette.chromeStroke),
boxShadow: [
_hovered ? palette.chromeShadowLift : palette.chromeShadowAmbient,
],
),
child: Material(
color: Colors.transparent,
@ -732,9 +775,9 @@ class _SidebarAccountTileState extends State<_SidebarAccountTile> {
Widget build(BuildContext context) {
final palette = context.palette;
final background = widget.selected
? palette.surfacePrimary
? palette.chromeSurface
: _hovered
? palette.surfaceTertiary
? palette.chromeSurfacePressed
: Colors.transparent;
return MouseRegion(
@ -745,11 +788,26 @@ class _SidebarAccountTileState extends State<_SidebarAccountTile> {
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
decoration: BoxDecoration(
color: background,
gradient: widget.selected || _hovered
? LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(
alpha: widget.selected ? 0.96 : 0.84,
),
background,
],
)
: null,
color: widget.selected || _hovered ? null : Colors.transparent,
borderRadius: BorderRadius.circular(AppRadius.button),
border: Border.all(
color: widget.selected ? palette.strokeSoft : Colors.transparent,
color: widget.selected || _hovered
? palette.chromeStroke
: Colors.transparent,
),
boxShadow: widget.selected ? [palette.chromeShadowLift] : const [],
),
child: Material(
color: Colors.transparent,
@ -858,11 +916,25 @@ class _SidebarLanguageButtonState extends State<_SidebarLanguageButton> {
height: size,
alignment: Alignment.center,
decoration: BoxDecoration(
color: _hovered
? palette.surfaceTertiary
: palette.surfaceSecondary,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(
alpha: _hovered ? 0.94 : 0.88,
),
_hovered
? palette.chromeSurfacePressed
: palette.chromeSurface,
],
),
borderRadius: BorderRadius.circular(AppRadius.button),
border: Border.all(color: palette.strokeSoft),
border: Border.all(color: palette.chromeStroke),
boxShadow: [
_hovered
? palette.chromeShadowLift
: palette.chromeShadowAmbient,
],
),
child: Text(
widget.appLanguage.compactLabel,

View File

@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
import '../theme/app_palette.dart';
import '../theme/app_theme.dart';
enum SurfaceCardTone { standard, chrome }
class SurfaceCard extends StatefulWidget {
const SurfaceCard({
super.key,
@ -11,6 +13,7 @@ class SurfaceCard extends StatefulWidget {
this.onTap,
this.borderRadius = AppRadius.card,
this.color,
this.tone = SurfaceCardTone.standard,
});
final Widget child;
@ -18,6 +21,7 @@ class SurfaceCard extends StatefulWidget {
final VoidCallback? onTap;
final double borderRadius;
final Color? color;
final SurfaceCardTone tone;
@override
State<SurfaceCard> createState() => _SurfaceCardState();
@ -29,7 +33,41 @@ class _SurfaceCardState extends State<SurfaceCard> {
@override
Widget build(BuildContext context) {
final palette = context.palette;
final baseColor = widget.color ?? palette.surfacePrimary;
final baseColor = switch (widget.tone) {
SurfaceCardTone.standard => widget.color ?? palette.surfacePrimary,
SurfaceCardTone.chrome => widget.color ?? palette.chromeSurface,
};
final hoveredColor = switch (widget.tone) {
SurfaceCardTone.standard => palette.surfaceSecondary,
SurfaceCardTone.chrome => palette.chromeSurfacePressed,
};
final borderColor = switch (widget.tone) {
SurfaceCardTone.standard => palette.strokeSoft,
SurfaceCardTone.chrome => palette.chromeStroke,
};
final decoration = switch (widget.tone) {
SurfaceCardTone.standard => BoxDecoration(
color: _hovered && widget.onTap != null ? hoveredColor : baseColor,
border: Border.all(color: borderColor),
borderRadius: BorderRadius.circular(widget.borderRadius),
),
SurfaceCardTone.chrome => BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(alpha: 0.94),
_hovered && widget.onTap != null ? hoveredColor : baseColor,
],
),
border: Border.all(color: borderColor),
borderRadius: BorderRadius.circular(widget.borderRadius),
boxShadow: [
palette.chromeShadowAmbient,
if (_hovered && widget.onTap != null) palette.chromeShadowLift,
],
),
};
return MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
@ -37,13 +75,7 @@ class _SurfaceCardState extends State<SurfaceCard> {
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
curve: Curves.easeInOut,
decoration: BoxDecoration(
color: _hovered && widget.onTap != null
? palette.surfaceSecondary
: baseColor,
border: Border.all(color: palette.strokeSoft),
borderRadius: BorderRadius.circular(widget.borderRadius),
),
decoration: decoration,
child: Material(
color: Colors.transparent,
child: InkWell(