add configurable simple theme defaults

This commit is contained in:
Haitao Pan 2026-03-18 19:12:37 +08:00
parent 098d9a2b19
commit febdbedbfb
5 changed files with 209 additions and 88 deletions

View File

@ -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`

View File

@ -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(

View File

@ -70,7 +70,7 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
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<GatewayConnectDialog> {
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<GatewayConnectDialog> {
),
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<GatewayConnectDialog> {
: '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<GatewayConnectDialog> {
});
},
),
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<GatewayConnectDialog> {
),
),
),
const SizedBox(width: 16),
const SizedBox(width: AppSpacing.section),
Expanded(
child: SwitchListTile.adaptive(
contentPadding: EdgeInsets.zero,
@ -175,7 +175,7 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
],
),
],
const SizedBox(height: 18),
const SizedBox(height: AppSpacing.section),
TextField(
controller: _tokenController,
obscureText: _obscureSharedToken,
@ -203,7 +203,7 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
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<GatewayConnectDialog> {
: null,
),
],
const SizedBox(height: 12),
const SizedBox(height: AppSpacing.section),
TextField(
controller: _passwordController,
obscureText: true,
@ -229,10 +229,10 @@ class _GatewayConnectDialogState extends State<GatewayConnectDialog> {
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<GatewayConnectDialog> {
}
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),
],
],

View File

@ -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,
),
),
),

View File

@ -38,13 +38,16 @@ class _SurfaceCardState extends State<SurfaceCard> {
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),
),
],
),