refactor: normalize desktop typography and density

This commit is contained in:
Haitao Pan 2026-03-13 15:52:20 +08:00
parent e7dabe2905
commit 14280a5005
7 changed files with 146 additions and 134 deletions

View File

@ -62,6 +62,7 @@ class AppTheme {
return base.copyWith(
splashFactory: NoSplash.splashFactory,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: isDesktop
? const VisualDensity(horizontal: -1, vertical: -1)
: VisualDensity.standard,
@ -82,27 +83,29 @@ class AppTheme {
shadowColor: palette.shadow,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: palette.strokeSoft),
),
),
chipTheme: base.chipTheme.copyWith(
backgroundColor: palette.surfaceSecondary,
side: BorderSide(color: palette.strokeSoft),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(999)),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
labelStyle: tunedTextTheme.labelMedium,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
textStyle: tunedTextTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w500,
),
minimumSize: Size(0, isDesktop ? 34 : 36),
padding: EdgeInsets.symmetric(
horizontal: isDesktop ? 16 : 18,
vertical: isDesktop ? 12 : 13,
horizontal: isDesktop ? 12 : 14,
vertical: isDesktop ? 8 : 9,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(999),
borderRadius: BorderRadius.circular(10),
),
),
),
@ -110,14 +113,15 @@ class AppTheme {
style: OutlinedButton.styleFrom(
foregroundColor: palette.textPrimary,
textStyle: tunedTextTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w500,
),
minimumSize: Size(0, isDesktop ? 34 : 36),
padding: EdgeInsets.symmetric(
horizontal: isDesktop ? 16 : 18,
vertical: isDesktop ? 12 : 13,
horizontal: isDesktop ? 12 : 14,
vertical: isDesktop ? 8 : 9,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(999),
borderRadius: BorderRadius.circular(10),
),
side: BorderSide(color: palette.strokeSoft),
),
@ -126,14 +130,15 @@ class AppTheme {
style: TextButton.styleFrom(
foregroundColor: palette.textPrimary,
textStyle: tunedTextTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w500,
),
minimumSize: Size(0, isDesktop ? 32 : 34),
padding: EdgeInsets.symmetric(
horizontal: isDesktop ? 12 : 14,
vertical: isDesktop ? 10 : 11,
horizontal: isDesktop ? 10 : 12,
vertical: isDesktop ? 8 : 9,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(10),
),
),
),
@ -142,10 +147,11 @@ class AppTheme {
foregroundColor: palette.textSecondary,
backgroundColor: palette.surfaceSecondary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(10),
side: BorderSide(color: palette.strokeSoft),
),
padding: const EdgeInsets.all(12),
minimumSize: const Size(34, 34),
padding: const EdgeInsets.all(8),
),
),
inputDecorationTheme: InputDecorationTheme(
@ -159,18 +165,18 @@ class AppTheme {
),
contentPadding: EdgeInsets.symmetric(
horizontal: isDesktop ? 16 : 18,
vertical: isDesktop ? 14 : 15,
vertical: isDesktop ? 12 : 13,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: palette.strokeSoft),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: palette.strokeSoft),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: palette.accent.withValues(alpha: 0.42)),
),
),
@ -190,10 +196,13 @@ class AppTheme {
return palette.textSecondary;
}),
padding: const WidgetStatePropertyAll(
EdgeInsets.symmetric(horizontal: 16, vertical: 12),
EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
textStyle: WidgetStatePropertyAll(
tunedTextTheme.labelLarge?.copyWith(fontWeight: FontWeight.w500),
),
),
),
@ -231,57 +240,56 @@ class AppTheme {
return base.copyWith(
displaySmall: withUiFont(
base.displaySmall?.copyWith(
fontSize: isDesktop ? 30 : 32,
fontWeight: FontWeight.w700,
letterSpacing: -0.8,
height: 1.08,
fontSize: isDesktop ? 22 : 24,
fontWeight: FontWeight.w600,
letterSpacing: -0.24,
height: 1.25,
),
),
headlineSmall: withUiFont(
base.headlineSmall?.copyWith(
fontSize: isDesktop ? 20 : 22,
fontWeight: FontWeight.w700,
letterSpacing: -0.38,
height: 1.14,
fontSize: isDesktop ? 22 : 24,
fontWeight: FontWeight.w600,
letterSpacing: -0.24,
height: 1.25,
),
),
titleLarge: withUiFont(
base.titleLarge?.copyWith(
fontSize: isDesktop ? 17 : 18,
fontSize: isDesktop ? 18 : 19,
fontWeight: FontWeight.w600,
letterSpacing: -0.18,
height: 1.2,
letterSpacing: -0.16,
height: 1.3,
),
),
titleMedium: withUiFont(
base.titleMedium?.copyWith(
fontSize: isDesktop ? 15 : 16,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.24,
letterSpacing: -0.08,
height: 1.35,
),
),
titleSmall: withUiFont(
base.titleSmall?.copyWith(
fontSize: isDesktop ? 13 : 14,
fontWeight: FontWeight.w600,
height: 1.2,
fontSize: isDesktop ? 14 : 15,
fontWeight: FontWeight.w500,
height: 1.4,
),
),
bodyLarge: withUiFont(
base.bodyLarge?.copyWith(
fontSize: isDesktop ? 14 : 15,
fontWeight: FontWeight.w400,
height: 1.5,
letterSpacing: -0.02,
height: 1.4,
color: palette.textPrimary,
),
),
bodyMedium: withUiFont(
base.bodyMedium?.copyWith(
fontSize: isDesktop ? 13 : 14,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.46,
letterSpacing: -0.01,
height: 1.4,
color: palette.textSecondary,
),
),
@ -289,30 +297,29 @@ class AppTheme {
base.bodySmall?.copyWith(
fontSize: isDesktop ? 12 : 13,
fontWeight: FontWeight.w400,
height: 1.4,
height: 1.45,
color: palette.textMuted,
),
),
labelLarge: withUiFont(
base.labelLarge?.copyWith(
fontSize: isDesktop ? 13 : 14,
fontWeight: FontWeight.w600,
height: 1.15,
letterSpacing: -0.02,
fontWeight: FontWeight.w500,
height: 1.2,
),
),
labelMedium: withUiFont(
base.labelMedium?.copyWith(
fontSize: isDesktop ? 12 : 12,
fontWeight: FontWeight.w600,
height: 1.12,
fontSize: 12,
fontWeight: FontWeight.w500,
height: 1.2,
),
),
labelSmall: withUiFont(
base.labelSmall?.copyWith(
fontSize: 11,
fontWeight: FontWeight.w600,
height: 1.1,
fontWeight: FontWeight.w500,
height: 1.2,
),
),
);

View File

@ -22,7 +22,7 @@ class SectionHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 6),
const SizedBox(height: 8),
Text(subtitle, style: Theme.of(context).textTheme.bodySmall),
],
),

View File

@ -24,19 +24,19 @@ class SectionTabs extends StatelessWidget {
final padding = switch (size) {
SectionTabsSize.small => const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
vertical: 6,
),
SectionTabsSize.medium => const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
horizontal: 12,
vertical: 8,
),
};
return Container(
padding: const EdgeInsets.all(6),
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: palette.surfaceSecondary,
borderRadius: BorderRadius.circular(18),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: palette.strokeSoft),
),
child: SingleChildScrollView(
@ -45,7 +45,7 @@ class SectionTabs extends StatelessWidget {
children: items.map((item) {
final selected = item == value;
return Padding(
padding: const EdgeInsets.only(right: 6),
padding: const EdgeInsets.only(right: 4),
child: _SectionTabChip(
label: item,
selected: selected,
@ -96,12 +96,12 @@ class _SectionTabChipState extends State<_SectionTabChip> {
: _hovered
? palette.hover
: Colors.transparent,
borderRadius: BorderRadius.circular(14),
borderRadius: BorderRadius.circular(10),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(14),
borderRadius: BorderRadius.circular(10),
onTap: widget.onTap,
child: Padding(
padding: widget.padding,

View File

@ -36,7 +36,7 @@ class SidebarNavigation extends StatelessWidget {
final String accountSubtitle;
final double? expandedWidthOverride;
static const _mainSections = [
static const _mainSections = <WorkspaceDestination>[
WorkspaceDestination.assistant,
WorkspaceDestination.tasks,
WorkspaceDestination.modules,
@ -67,10 +67,7 @@ class SidebarNavigation extends StatelessWidget {
),
),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: isExpanded ? 10 : 8,
vertical: 10,
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@ -78,9 +75,9 @@ class SidebarNavigation extends StatelessWidget {
isCollapsed: !isExpanded,
onTap: isCollapsed ? onExpandFromCollapsed : null,
),
const SizedBox(height: 12),
const SizedBox(height: 8),
Container(height: 1, color: palette.sidebarBorder),
const SizedBox(height: 12),
const SizedBox(height: 8),
Expanded(
child: ListView(
padding: EdgeInsets.zero,
@ -98,7 +95,7 @@ class SidebarNavigation extends StatelessWidget {
),
const SizedBox(height: 8),
Container(height: 1, color: palette.sidebarBorder),
const SizedBox(height: 10),
const SizedBox(height: 8),
SidebarFooter(
isCollapsed: isCollapsed,
appLanguage: appLanguage,
@ -136,13 +133,13 @@ class SidebarHeader extends StatelessWidget {
final palette = context.palette;
final content = Container(
width: isCollapsed ? 38 : 34,
height: isCollapsed ? 38 : 34,
width: isCollapsed ? 36 : 32,
height: isCollapsed ? 36 : 32,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(10),
color: palette.accentMuted,
),
child: Icon(Icons.auto_awesome_rounded, color: palette.accent, size: 20),
child: Icon(Icons.auto_awesome_rounded, color: palette.accent, size: 18),
);
if (onTap == null) {
@ -152,10 +149,10 @@ class SidebarHeader extends StatelessWidget {
return Tooltip(
message: appText('展开导航', 'Expand sidebar'),
child: InkWell(
borderRadius: BorderRadius.circular(14),
borderRadius: BorderRadius.circular(10),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.symmetric(vertical: 2),
child: content,
),
),
@ -199,6 +196,7 @@ class _SidebarNavItemState extends State<SidebarNavItem> {
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),
@ -211,7 +209,7 @@ class _SidebarNavItemState extends State<SidebarNavItem> {
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: widget.collapsed ? 0 : 10,
vertical: widget.collapsed ? 10 : 9,
vertical: 0,
),
child: Row(
mainAxisAlignment: widget.collapsed
@ -221,11 +219,15 @@ class _SidebarNavItemState extends State<SidebarNavItem> {
Icon(widget.section.icon, color: foreground, size: 18),
if (!widget.collapsed) ...[
const SizedBox(width: 8),
Text(
widget.section.label,
style: Theme.of(
context,
).textTheme.labelLarge?.copyWith(color: foreground),
Expanded(
child: Text(
widget.section.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(
context,
).textTheme.labelLarge?.copyWith(color: foreground),
),
),
],
],
@ -286,19 +288,11 @@ class SidebarFooter extends StatelessWidget {
AppSidebarState.collapsed => appText('隐藏导航', 'Hide sidebar'),
AppSidebarState.hidden => appText('展开导航', 'Expand sidebar'),
};
final languageButton = Tooltip(
message: appText('切换语言', 'Switch language'),
child: _SidebarLanguageButton(
appLanguage: appLanguage,
compact: isCollapsed,
onPressed: onToggleLanguage,
),
);
final themeButton = Tooltip(
message: themeLabel,
child: IconButton(
iconSize: 20,
iconSize: 18,
onPressed: onOpenThemeToggle,
icon: Icon(
themeMode == ThemeMode.dark
@ -308,10 +302,19 @@ class SidebarFooter extends StatelessWidget {
),
);
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: 20,
iconSize: 18,
onPressed: onOpenSettings,
icon: const Icon(Icons.settings_rounded),
),
@ -320,7 +323,7 @@ class SidebarFooter extends StatelessWidget {
final collapseButton = Tooltip(
message: collapseLabel,
child: IconButton(
iconSize: 20,
iconSize: 18,
onPressed: onCycleSidebarState,
icon: Icon(switch (sidebarState) {
AppSidebarState.expanded => Icons.keyboard_double_arrow_left_rounded,
@ -338,11 +341,11 @@ class SidebarFooter extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
themeButton,
const SizedBox(height: 6),
const SizedBox(height: 4),
languageButton,
const SizedBox(height: 6),
const SizedBox(height: 4),
settingsButton,
const SizedBox(height: 6),
const SizedBox(height: 4),
collapseButton,
],
)
@ -357,20 +360,20 @@ class SidebarFooter extends StatelessWidget {
label: themeLabel,
onTap: onOpenThemeToggle,
),
const SizedBox(height: 6),
const SizedBox(height: 4),
_SidebarFooterActionTile(
icon: Icons.translate_rounded,
label: appText('语言', 'Language'),
trailingText: appLanguage == AppLanguage.zh ? '中文' : 'EN',
onTap: onToggleLanguage,
),
const SizedBox(height: 6),
const SizedBox(height: 4),
_SidebarFooterActionTile(
icon: Icons.settings_rounded,
label: appText('打开设置', 'Open settings'),
onTap: onOpenSettings,
),
const SizedBox(height: 6),
const SizedBox(height: 4),
_SidebarFooterActionTile(
icon: switch (sidebarState) {
AppSidebarState.expanded =>
@ -384,24 +387,24 @@ class SidebarFooter extends StatelessWidget {
),
],
),
const SizedBox(height: 8),
const SizedBox(height: 12),
if (isCollapsed)
Tooltip(
message: appText('账号', 'Account'),
child: InkWell(
borderRadius: BorderRadius.circular(18),
borderRadius: BorderRadius.circular(10),
onTap: onOpenAccount,
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10),
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: accountSelected
? palette.accentMuted
: palette.surfaceSecondary,
borderRadius: BorderRadius.circular(14),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: palette.strokeSoft),
),
child: const Icon(Icons.account_circle_rounded),
child: const Icon(Icons.account_circle_rounded, size: 20),
),
),
)
@ -451,33 +454,35 @@ class _SidebarFooterActionTileState extends State<_SidebarFooterActionTile> {
duration: const Duration(milliseconds: 160),
decoration: BoxDecoration(
color: _hovered ? palette.hover : Colors.transparent,
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(10),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(10),
onTap: widget.onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(widget.icon, size: 20, color: palette.textSecondary),
Icon(widget.icon, size: 18, color: palette.textSecondary),
const SizedBox(width: 8),
Text(
widget.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.labelLarge,
Flexible(
child: Text(
widget.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.labelLarge,
),
),
if (widget.trailingText != null) ...[
const SizedBox(width: 12),
const SizedBox(width: 8),
Text(
widget.trailingText!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: palette.textSecondary,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w500,
),
),
],
@ -530,12 +535,12 @@ class _SidebarAccountTileState extends State<_SidebarAccountTile> {
duration: const Duration(milliseconds: 160),
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(14),
borderRadius: BorderRadius.circular(10),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(14),
borderRadius: BorderRadius.circular(10),
onTap: widget.onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
@ -562,7 +567,7 @@ class _SidebarAccountTileState extends State<_SidebarAccountTile> {
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 1),
const SizedBox(height: 2),
Text(
widget.subtitle,
maxLines: 1,
@ -604,13 +609,13 @@ class _SidebarLanguageButtonState extends State<_SidebarLanguageButton> {
@override
Widget build(BuildContext context) {
final palette = context.palette;
final size = widget.compact ? 44.0 : 58.0;
final size = widget.compact ? 36.0 : 44.0;
return MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: InkWell(
borderRadius: BorderRadius.circular(18),
borderRadius: BorderRadius.circular(10),
onTap: widget.onPressed,
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
@ -619,14 +624,14 @@ class _SidebarLanguageButtonState extends State<_SidebarLanguageButton> {
alignment: Alignment.center,
decoration: BoxDecoration(
color: _hovered ? palette.hover : palette.surfaceSecondary,
borderRadius: BorderRadius.circular(18),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: palette.strokeSoft),
),
child: Text(
widget.appLanguage.compactLabel,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: palette.textPrimary,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w600,
),
),
),

View File

@ -31,18 +31,18 @@ class StatusBadge extends StatelessWidget {
return Container(
padding: EdgeInsets.symmetric(
horizontal: compact ? 10 : 12,
vertical: compact ? 5 : 7,
horizontal: compact ? 8 : 10,
vertical: compact ? 4 : 6,
),
decoration: BoxDecoration(
color: tone.$1,
borderRadius: BorderRadius.circular(999),
borderRadius: BorderRadius.circular(10),
),
child: Text(
status.label,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: tone.$2,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w500,
),
),
);

View File

@ -42,9 +42,9 @@ class _SurfaceCardState extends State<SurfaceCard> {
border: Border.all(color: palette.strokeSoft),
boxShadow: [
BoxShadow(
color: palette.shadow.withValues(alpha: _hovered ? 0.12 : 0.07),
blurRadius: _hovered ? 12 : 8,
offset: const Offset(0, 4),
color: palette.shadow.withValues(alpha: _hovered ? 0.08 : 0.05),
blurRadius: _hovered ? 10 : 6,
offset: const Offset(0, 3),
),
],
),

View File

@ -23,9 +23,9 @@ class TopBar extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.headlineSmall),
const SizedBox(height: 6),
Text(subtitle, style: Theme.of(context).textTheme.bodyLarge),
if (trailing != null) ...[const SizedBox(height: 12), trailing!],
const SizedBox(height: 8),
Text(subtitle, style: Theme.of(context).textTheme.bodyMedium),
if (trailing != null) ...[const SizedBox(height: 16), trailing!],
],
);
}
@ -38,13 +38,13 @@ class TopBar extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.headlineSmall),
const SizedBox(height: 6),
Text(subtitle, style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 8),
Text(subtitle, style: Theme.of(context).textTheme.bodyMedium),
],
),
),
if (trailing != null) ...[
const SizedBox(width: 16),
const SizedBox(width: 24),
Flexible(child: trailing!),
],
],