From febdbedbfba33104a3f521c96daeba701e644cc6 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Wed, 18 Mar 2026 19:12:37 +0800 Subject: [PATCH] add configurable simple theme defaults --- docs/architecture/simple-theme-default.md | 33 +++++ lib/theme/app_theme.dart | 170 +++++++++++++++++----- lib/widgets/gateway_connect_dialog.dart | 55 +++---- lib/widgets/section_tabs.dart | 28 ++-- lib/widgets/surface_card.dart | 11 +- 5 files changed, 209 insertions(+), 88 deletions(-) create mode 100644 docs/architecture/simple-theme-default.md diff --git a/docs/architecture/simple-theme-default.md b/docs/architecture/simple-theme-default.md new file mode 100644 index 00000000..92ceeb52 --- /dev/null +++ b/docs/architecture/simple-theme-default.md @@ -0,0 +1,33 @@ +# Simple Theme Default + +This document records the default `simple` theme token set for `XWorkmate.svc.plus`. + +## Typography + +- `section` title: `13/14 + 600` +- `compact body`: `13/15 + 400` +- `bodyMedium`: `13/15 + 400` +- emphasized body and button labels: `13/14 + 600` + +## Spacing + +- page outer spacing: `0` +- standard section spacing: `8` +- compact spacing: `6` + +## Radius + +- card radius: `6` +- input radius: `8` +- button radius: `8` +- dialog radius: `5` + +## Size + +- input height: `36` +- button height: `16` + +## Source Of Truth + +- theme tokens live in [lib/theme/app_theme.dart](/Users/shenlan/workspaces/cloud-neutral-toolkit/XWorkmate.svc.plus/lib/theme/app_theme.dart) +- token family name: `simple` diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index a693d78f..48d500c4 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -3,32 +3,36 @@ import 'package:flutter/material.dart'; import 'app_palette.dart'; -class AppSpacing { - AppSpacing._(); +// Default theme token set: simple +class SimpleSpacing { + SimpleSpacing._(); + static const double page = 0.0; + static const double compact = 6.0; + static const double section = 8.0; static const double xxs = 4.0; - static const double xs = 8.0; - static const double sm = 12.0; - static const double md = 16.0; - static const double lg = 24.0; - static const double xl = 32.0; + static const double xs = compact; + static const double sm = section; + static const double md = section; + static const double lg = section; + static const double xl = 12.0; } -class AppRadius { - AppRadius._(); +class SimpleRadius { + SimpleRadius._(); - static const double card = 16.0; - static const double button = 12.0; - static const double input = 16.0; + static const double card = 6.0; + static const double button = 8.0; + static const double input = 8.0; static const double chip = 999.0; static const double badge = 999.0; - static const double dialog = 16.0; - static const double sidebar = 20.0; - static const double icon = 12.0; + static const double dialog = 5.0; + static const double sidebar = 8.0; + static const double icon = 8.0; } -class AppTypography { - AppTypography._(); +class SimpleTypography { + SimpleTypography._(); static const double displaySize = 28.0; static const FontWeight displayWeight = FontWeight.w600; @@ -42,25 +46,25 @@ class AppTypography { static const FontWeight sectionWeight = FontWeight.w600; static const double sectionHeight = 14 / 13; - static const double bodySize = 14.0; + static const double bodySize = 13.0; static const FontWeight bodyWeight = FontWeight.w400; - static const double bodyHeight = 20 / 14; + static const double bodyHeight = 15 / 13; static const double compactBodySize = 13.0; static const FontWeight compactBodyWeight = FontWeight.w400; static const double compactBodyHeight = 15 / 13; - static const double emphasizedBodySize = 14.0; + static const double emphasizedBodySize = 13.0; static const FontWeight emphasizedBodyWeight = FontWeight.w600; - static const double emphasizedBodyHeight = 14 / 14; + static const double emphasizedBodyHeight = 14 / 13; static const double captionSize = 12.0; static const FontWeight captionWeight = FontWeight.w400; static const double captionHeight = 16 / 12; } -class AppSizes { - AppSizes._(); +class SimpleSizes { + SimpleSizes._(); static const double sidebarItemHeight = 40.0; static const double sidebarIconSize = 20.0; @@ -68,11 +72,91 @@ class AppSizes { static const double sidebarExpandedWidth = 212.0; static const double sidebarCollapsedWidth = 72.0; - static const double textareaHeight = 48.0; + static const double textareaHeight = 36.0; static const double toolbarHeight = 40.0; - static const double buttonHeightDesktop = 40.0; - static const double buttonHeightMobile = 40.0; + static const double inputHeight = 36.0; + static const double buttonHeightDesktop = 16.0; + static const double buttonHeightMobile = 16.0; +} + +class AppSpacing { + AppSpacing._(); + + static const double page = SimpleSpacing.page; + static const double compact = SimpleSpacing.compact; + static const double section = SimpleSpacing.section; + static const double xxs = SimpleSpacing.xxs; + static const double xs = SimpleSpacing.xs; + static const double sm = SimpleSpacing.sm; + static const double md = SimpleSpacing.md; + static const double lg = SimpleSpacing.lg; + static const double xl = SimpleSpacing.xl; +} + +class AppRadius { + AppRadius._(); + + static const double card = SimpleRadius.card; + static const double button = SimpleRadius.button; + static const double input = SimpleRadius.input; + static const double chip = SimpleRadius.chip; + static const double badge = SimpleRadius.badge; + static const double dialog = SimpleRadius.dialog; + static const double sidebar = SimpleRadius.sidebar; + static const double icon = SimpleRadius.icon; +} + +class AppTypography { + AppTypography._(); + + static const double displaySize = SimpleTypography.displaySize; + static const FontWeight displayWeight = SimpleTypography.displayWeight; + static const double displayHeight = SimpleTypography.displayHeight; + + static const double titleSize = SimpleTypography.titleSize; + static const FontWeight titleWeight = SimpleTypography.titleWeight; + static const double titleHeight = SimpleTypography.titleHeight; + + static const double sectionSize = SimpleTypography.sectionSize; + static const FontWeight sectionWeight = SimpleTypography.sectionWeight; + static const double sectionHeight = SimpleTypography.sectionHeight; + + static const double bodySize = SimpleTypography.bodySize; + static const FontWeight bodyWeight = SimpleTypography.bodyWeight; + static const double bodyHeight = SimpleTypography.bodyHeight; + + static const double compactBodySize = SimpleTypography.compactBodySize; + static const FontWeight compactBodyWeight = + SimpleTypography.compactBodyWeight; + static const double compactBodyHeight = SimpleTypography.compactBodyHeight; + + static const double emphasizedBodySize = SimpleTypography.emphasizedBodySize; + static const FontWeight emphasizedBodyWeight = + SimpleTypography.emphasizedBodyWeight; + static const double emphasizedBodyHeight = + SimpleTypography.emphasizedBodyHeight; + + static const double captionSize = SimpleTypography.captionSize; + static const FontWeight captionWeight = SimpleTypography.captionWeight; + static const double captionHeight = SimpleTypography.captionHeight; +} + +class AppSizes { + AppSizes._(); + + static const double sidebarItemHeight = SimpleSizes.sidebarItemHeight; + static const double sidebarIconSize = SimpleSizes.sidebarIconSize; + static const double sidebarTextSize = SimpleSizes.sidebarTextSize; + static const double sidebarExpandedWidth = SimpleSizes.sidebarExpandedWidth; + static const double sidebarCollapsedWidth = SimpleSizes.sidebarCollapsedWidth; + + static const double textareaHeight = SimpleSizes.textareaHeight; + static const double toolbarHeight = SimpleSizes.toolbarHeight; + + static const double inputHeight = SimpleSizes.inputHeight; + static const double buttonHeightDesktop = SimpleSizes.buttonHeightDesktop; + static const double buttonHeightMobile = SimpleSizes.buttonHeightMobile; } class AppTheme { @@ -152,6 +236,7 @@ class AppTheme { surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.card), + side: BorderSide(color: palette.strokeSoft), ), ), chipTheme: base.chipTheme.copyWith( @@ -184,8 +269,8 @@ class AppTheme { : AppSizes.buttonHeightMobile, ), padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.sm, + horizontal: AppSpacing.sm, + vertical: 0, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.button), @@ -209,13 +294,13 @@ class AppTheme { : AppSizes.buttonHeightMobile, ), padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.sm, + horizontal: AppSpacing.sm, + vertical: 0, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.button), ), - side: BorderSide.none, + side: BorderSide(color: palette.strokeSoft), ), ), textButtonTheme: TextButtonThemeData( @@ -224,10 +309,15 @@ class AppTheme { textStyle: tunedTextTheme.labelLarge?.copyWith( fontWeight: FontWeight.w500, ), - minimumSize: Size(0, isDesktop ? 32 : 34), + minimumSize: Size( + 0, + isDesktop + ? AppSizes.buttonHeightDesktop + : AppSizes.buttonHeightMobile, + ), padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.xs, + horizontal: AppSpacing.xs, + vertical: 0, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.button), @@ -247,6 +337,7 @@ class AppTheme { ), ), inputDecorationTheme: InputDecorationTheme( + isDense: true, filled: true, fillColor: palette.surfacePrimary, hintStyle: tunedTextTheme.bodyMedium?.copyWith( @@ -259,16 +350,17 @@ class AppTheme { color: palette.textSecondary, ), contentPadding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.sm, + horizontal: AppSpacing.sm, + vertical: AppSpacing.compact, ), + constraints: const BoxConstraints(minHeight: AppSizes.inputHeight), border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.input), - borderSide: const BorderSide(color: Colors.transparent), + borderSide: BorderSide(color: palette.strokeSoft), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.input), - borderSide: const BorderSide(color: Colors.transparent), + borderSide: BorderSide(color: palette.strokeSoft), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.input), @@ -293,7 +385,7 @@ class AppTheme { padding: const WidgetStatePropertyAll( EdgeInsets.symmetric( horizontal: AppSpacing.sm, - vertical: AppSpacing.xs, + vertical: AppSpacing.compact, ), ), shape: WidgetStatePropertyAll( diff --git a/lib/widgets/gateway_connect_dialog.dart b/lib/widgets/gateway_connect_dialog.dart index 43979086..e3984f65 100644 --- a/lib/widgets/gateway_connect_dialog.dart +++ b/lib/widgets/gateway_connect_dialog.dart @@ -70,7 +70,7 @@ class _GatewayConnectDialogState extends State { final willUseStoredGatewayToken = typedGatewayToken.isEmpty && hasStoredGatewayToken; final body = SingleChildScrollView( - padding: const EdgeInsets.all(24), + padding: const EdgeInsets.all(AppSpacing.page), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -79,7 +79,7 @@ class _GatewayConnectDialogState extends State { appText('Gateway 访问', 'Gateway Access'), style: theme.textTheme.headlineSmall, ), - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.section), Text( appText( '通过配置码或手动 Host / TLS 将 XWorkmate 连接到 OpenClaw Gateway。', @@ -87,7 +87,7 @@ class _GatewayConnectDialogState extends State { ), style: theme.textTheme.bodyMedium, ), - const SizedBox(height: 18), + const SizedBox(height: AppSpacing.section), SectionTabs( items: [appText('配置码', 'Setup Code'), appText('手动配置', 'Manual')], value: _mode == 'setup' @@ -100,9 +100,9 @@ class _GatewayConnectDialogState extends State { : 'manual', ), ), - const SizedBox(height: 18), + const SizedBox(height: AppSpacing.section), _StatusBanner(controller: widget.controller), - const SizedBox(height: 18), + const SizedBox(height: AppSpacing.section), if (_mode == 'setup') ...[ TextField( controller: _setupCodeController, @@ -144,12 +144,12 @@ class _GatewayConnectDialogState extends State { }); }, ), - const SizedBox(height: 12), + const SizedBox(height: AppSpacing.section), TextField( controller: _hostController, decoration: InputDecoration(labelText: appText('主机', 'Host')), ), - const SizedBox(height: 12), + const SizedBox(height: AppSpacing.section), Row( children: [ Expanded( @@ -161,7 +161,7 @@ class _GatewayConnectDialogState extends State { ), ), ), - const SizedBox(width: 16), + const SizedBox(width: AppSpacing.section), Expanded( child: SwitchListTile.adaptive( contentPadding: EdgeInsets.zero, @@ -175,7 +175,7 @@ class _GatewayConnectDialogState extends State { ], ), ], - const SizedBox(height: 18), + const SizedBox(height: AppSpacing.section), TextField( controller: _tokenController, obscureText: _obscureSharedToken, @@ -203,7 +203,7 @@ class _GatewayConnectDialogState extends State { onChanged: (_) => setState(() {}), ), if (willUseStoredGatewayToken || typedGatewayToken.isNotEmpty) ...[ - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.section), _SharedTokenStatusCard( hasStoredGatewayToken: hasStoredGatewayToken, storedGatewayTokenMask: storedGatewayTokenMask, @@ -220,7 +220,7 @@ class _GatewayConnectDialogState extends State { : null, ), ], - const SizedBox(height: 12), + const SizedBox(height: AppSpacing.section), TextField( controller: _passwordController, obscureText: true, @@ -229,10 +229,10 @@ class _GatewayConnectDialogState extends State { hintText: appText('可选:共享密码', 'Optional shared password'), ), ), - const SizedBox(height: 20), + const SizedBox(height: AppSpacing.section), Wrap( - spacing: 12, - runSpacing: 12, + spacing: AppSpacing.section, + runSpacing: AppSpacing.section, alignment: WrapAlignment.end, children: [ if (widget.controller.connection.status == @@ -270,6 +270,7 @@ class _GatewayConnectDialogState extends State { } return Dialog( + insetPadding: const EdgeInsets.all(AppSpacing.page), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 560), child: body, @@ -373,10 +374,11 @@ class _SharedTokenStatusCard extends StatelessWidget { ); return Container( width: double.infinity, - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(AppSpacing.section), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(16), + border: Border.all(color: theme.dividerColor), + borderRadius: BorderRadius.circular(AppRadius.card), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -387,7 +389,7 @@ class _SharedTokenStatusCard extends StatelessWidget { : Icons.inventory_2_rounded, size: 18, ), - const SizedBox(width: 10), + const SizedBox(width: AppSpacing.compact), Expanded(child: Text(message, style: theme.textTheme.bodySmall)), if (onClearStoredToken != null) TextButton( @@ -419,29 +421,30 @@ class _StatusBanner extends StatelessWidget { }; return Container( width: double.infinity, - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(AppSpacing.section), decoration: BoxDecoration( color: tone, - borderRadius: BorderRadius.circular(18), + border: Border.all(color: theme.dividerColor), + borderRadius: BorderRadius.circular(AppRadius.card), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(connection.status.label, style: theme.textTheme.titleMedium), - const SizedBox(height: 6), + const SizedBox(height: AppSpacing.compact), Text( connection.remoteAddress ?? 'No active gateway target', style: theme.textTheme.bodyMedium, ), - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.section), Text( appText('认证诊断', 'Auth Diagnostics'), style: theme.textTheme.labelLarge, ), - const SizedBox(height: 4), + const SizedBox(height: AppSpacing.compact), Text(connection.connectAuthSummary, style: theme.textTheme.bodySmall), if (connection.pairingRequired) ...[ - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.section), Text( appText( '当前设备需要先完成配对审批。请在已授权设备上批准该请求后重试。', @@ -450,7 +453,7 @@ class _StatusBanner extends StatelessWidget { style: theme.textTheme.bodySmall, ), if ((connection.deviceId ?? '').isNotEmpty) ...[ - const SizedBox(height: 6), + const SizedBox(height: AppSpacing.compact), Text( appText( '当前设备 ID: ${connection.deviceId}', @@ -460,7 +463,7 @@ class _StatusBanner extends StatelessWidget { ), ], ] else if (connection.gatewayTokenMissing) ...[ - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.section), Text( appText( '首次连接请提供共享 Token;配对完成后可继续使用本机 device token。', @@ -470,7 +473,7 @@ class _StatusBanner extends StatelessWidget { ), ], if ((connection.lastError ?? '').isNotEmpty) ...[ - const SizedBox(height: 8), + const SizedBox(height: AppSpacing.section), Text(connection.lastError!, style: theme.textTheme.bodySmall), ], ], diff --git a/lib/widgets/section_tabs.dart b/lib/widgets/section_tabs.dart index 09d9ae31..07869b6c 100644 --- a/lib/widgets/section_tabs.dart +++ b/lib/widgets/section_tabs.dart @@ -25,11 +25,11 @@ class SectionTabs extends StatelessWidget { final padding = switch (size) { SectionTabsSize.small => const EdgeInsets.symmetric( horizontal: AppSpacing.sm, - vertical: 6, + vertical: AppSpacing.compact, ), SectionTabsSize.medium => const EdgeInsets.symmetric( horizontal: AppSpacing.sm, - vertical: AppSpacing.xs, + vertical: AppSpacing.compact, ), }; @@ -38,13 +38,7 @@ class SectionTabs extends StatelessWidget { decoration: BoxDecoration( color: palette.surfaceSecondary, borderRadius: BorderRadius.circular(AppRadius.chip), - boxShadow: [ - BoxShadow( - color: palette.shadow.withValues(alpha: 0.04), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], + border: Border.all(color: palette.strokeSoft), ), child: SingleChildScrollView( scrollDirection: Axis.horizontal, @@ -104,15 +98,9 @@ class _SectionTabChipState extends State<_SectionTabChip> { ? palette.surfaceTertiary : Colors.transparent, borderRadius: BorderRadius.circular(AppRadius.button), - boxShadow: widget.selected - ? [ - BoxShadow( - color: palette.shadow.withValues(alpha: 0.06), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ] - : const [], + border: Border.all( + color: widget.selected ? palette.stroke : Colors.transparent, + ), ), child: Material( color: Colors.transparent, @@ -127,7 +115,9 @@ class _SectionTabChipState extends State<_SectionTabChip> { color: widget.selected ? palette.textPrimary : palette.textSecondary, - fontWeight: widget.selected ? FontWeight.w600 : FontWeight.w500, + fontWeight: widget.selected + ? FontWeight.w600 + : FontWeight.w500, ), ), ), diff --git a/lib/widgets/surface_card.dart b/lib/widgets/surface_card.dart index fd9f8624..916a6cb3 100644 --- a/lib/widgets/surface_card.dart +++ b/lib/widgets/surface_card.dart @@ -38,13 +38,16 @@ class _SurfaceCardState extends State { duration: const Duration(milliseconds: 180), curve: Curves.easeInOut, decoration: BoxDecoration( - color: _hovered && widget.onTap != null ? palette.surfaceSecondary : baseColor, + color: _hovered && widget.onTap != null + ? palette.surfaceSecondary + : baseColor, + border: Border.all(color: palette.strokeSoft), borderRadius: BorderRadius.circular(widget.borderRadius), boxShadow: [ BoxShadow( - color: palette.shadow.withValues(alpha: _hovered ? 0.10 : 0.06), - blurRadius: _hovered ? 12 : 8, - offset: const Offset(0, 2), + color: palette.shadow.withValues(alpha: _hovered ? 0.04 : 0.02), + blurRadius: _hovered ? 6 : 4, + offset: const Offset(0, 1), ), ], ),