fix(ui): unify text field appearance

This commit is contained in:
Levi committed 2026-09-06 22:26:35 +08:00
1 parent ecfd07ccb0
commit a834ca0d74
7 files changed
+83 -57

No files matched your search

@@ -356,18 +356,12 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
const SizedBox(height: 16),
_FormRow(
label: context.l10n.connections,
child: Align(
alignment: Alignment.centerLeft,
child: SizedBox(
width: AppDesignTokens.settingsNumberControlWidth,
child: AppNumberInput(
fieldKey: const ValueKey('create-task-connections-input'),
controller: _connectionsController,
min: 1,
max: 256,
hintText: context.l10n.enterCount,
),
),
child: AppNumberInput(
fieldKey: const ValueKey('create-task-connections-input'),
controller: _connectionsController,
min: 1,
max: 256,
hintText: context.l10n.enterCount,
),
),
const SizedBox(height: 16),
@@ -382,10 +376,6 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
pickerKey: const ValueKey('create-task-directory-picker'),
controller: _directoryController,
hintText: context.l10n.chooseDownloadDirectory,
filled: true,
border: Border.all(color: palette.border),
borderRadius: BorderRadius.circular(AppDesignTokens.controlRadius),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
onChanged: (_) {
if (_asDefaultPath) {
setState(() => _asDefaultPath = false);
@@ -1654,17 +1644,12 @@ class _WindowTextField extends StatelessWidget {
@override
Widget build(BuildContext context) {
final palette = AppPalette.of(context);
return AppTextField(
controller: controller,
hintText: hintText,
keyboardType: keyboardType,
inputFormatters: inputFormatters,
obscureText: obscureText,
filled: true,
border: Border.all(color: palette.border),
borderRadius: BorderRadius.circular(4),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
);
}
}
@@ -18,42 +18,50 @@ class AppComponentThemes extends StatelessWidget {
align: 1,
border: Border.all(color: theme.colorScheme.ring.withValues(alpha: 0.65), width: 1),
),
child: shad.ComponentTheme<shad.SecondaryButtonTheme>(
data: shad.SecondaryButtonTheme(
decoration: (context, states, value) {
final decoration = switch (value) {
final BoxDecoration decoration => decoration,
_ => BoxDecoration(borderRadius: BorderRadius.circular(theme.radiusMd)),
};
if (states.contains(WidgetState.disabled)) {
return decoration.copyWith(color: palette.surfaceSoft);
}
if (theme.brightness == Brightness.light) {
final background = Color.alphaBlend(palette.textPrimary.withValues(alpha: 0.08), palette.surfaceSoft);
final hoverBackground = Color.alphaBlend(
palette.textPrimary.withValues(alpha: 0.13),
palette.surfaceSoft,
);
return decoration.copyWith(color: states.contains(WidgetState.hovered) ? hoverBackground : background);
}
return value;
},
textStyle: (context, states, value) => states.contains(WidgetState.disabled)
? value.copyWith(color: palette.textMuted.withValues(alpha: 0.62))
: value,
iconTheme: (context, states, value) => states.contains(WidgetState.disabled)
? value.copyWith(color: palette.textMuted.withValues(alpha: 0.62))
: value,
child: shad.ComponentTheme<shad.TextFieldTheme>(
data: shad.TextFieldTheme(
filled: true,
border: Border.all(color: palette.border),
borderRadius: BorderRadius.circular(AppDesignTokens.controlRadius),
padding: AppDesignTokens.textFieldPadding,
),
child: shad.ComponentTheme<shad.CheckboxTheme>(
data: shad.CheckboxTheme(
size: AppDesignTokens.checkboxSize,
gap: AppDesignTokens.checkboxLabelGap,
borderColor: theme.brightness == Brightness.dark ? palette.textMuted : palette.border,
child: shad.ComponentTheme<shad.SecondaryButtonTheme>(
data: shad.SecondaryButtonTheme(
decoration: (context, states, value) {
final decoration = switch (value) {
final BoxDecoration decoration => decoration,
_ => BoxDecoration(borderRadius: BorderRadius.circular(theme.radiusMd)),
};
if (states.contains(WidgetState.disabled)) {
return decoration.copyWith(color: palette.surfaceSoft);
}
if (theme.brightness == Brightness.light) {
final background = Color.alphaBlend(palette.textPrimary.withValues(alpha: 0.08), palette.surfaceSoft);
final hoverBackground = Color.alphaBlend(
palette.textPrimary.withValues(alpha: 0.13),
palette.surfaceSoft,
);
return decoration.copyWith(color: states.contains(WidgetState.hovered) ? hoverBackground : background);
}
return value;
},
textStyle: (context, states, value) => states.contains(WidgetState.disabled)
? value.copyWith(color: palette.textMuted.withValues(alpha: 0.62))
: value,
iconTheme: (context, states, value) => states.contains(WidgetState.disabled)
? value.copyWith(color: palette.textMuted.withValues(alpha: 0.62))
: value,
),
child: shad.ComponentTheme<shad.ToastTheme>(
data: const shad.ToastTheme(toastConstraints: BoxConstraints(maxWidth: 420)),
child: child,
child: shad.ComponentTheme<shad.CheckboxTheme>(
data: shad.CheckboxTheme(
size: AppDesignTokens.checkboxSize,
gap: AppDesignTokens.checkboxLabelGap,
borderColor: theme.brightness == Brightness.dark ? palette.textMuted : palette.border,
),
child: shad.ComponentTheme<shad.ToastTheme>(
data: const shad.ToastTheme(toastConstraints: BoxConstraints(maxWidth: 420)),
child: child,
),
),
),
),
@@ -21,6 +21,8 @@ class AppDesignTokens {
static const double settingsFormControlWidth = 420;
static const double settingsNumberControlWidth = 168;
static const double settingsItemMinLabelWidth = 160;
static const EdgeInsets textFieldPadding = EdgeInsets.symmetric(horizontal: 12, vertical: 11);
static const EdgeInsets numberTextFieldPadding = EdgeInsets.symmetric(horizontal: 12, vertical: 10);
static const EdgeInsets pagePadding = EdgeInsets.symmetric(horizontal: 32);
static const EdgeInsets sidebarPadding = EdgeInsets.symmetric(horizontal: 16);
@@ -2,6 +2,7 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
import '../theme/app_design_tokens.dart';
import 'app_text_field.dart';
/// A bounded numeric text field with the app's standard spinner controls.
@@ -91,6 +92,7 @@ class _AppNumberInputState extends State<AppNumberInput> {
: TextInputType.number,
inputFormatters: formatters,
enabled: widget.enabled,
padding: AppDesignTokens.numberTextFieldPadding,
features: [
shad.InputFeature.spinner(
step: widget.step,
@@ -158,7 +158,10 @@ void main() {
final connectionsInput = find.byKey(const ValueKey('create-task-connections-input'));
expect(connectionsInput, findsOneWidget);
expect(tester.getSize(connectionsInput).width, AppDesignTokens.settingsNumberControlWidth);
expect(
tester.getSize(connectionsInput).width,
tester.getSize(find.byKey(const ValueKey('create-task-rename-input'))).width,
);
final connectionsField = tester.widget<shad.TextField>(connectionsInput);
expect(connectionsField.controller!.text, '12');
expect(connectionsField.features.single, isA<shad.InputSpinnerFeature>());
@@ -7,6 +7,30 @@ import 'package:gopeed/shared/theme/app_theme.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
void main() {
testWidgets('provides the standard text field appearance', (tester) async {
late shad.TextFieldTheme textFieldTheme;
await tester.pumpWidget(
shad.ShadcnApp(
theme: AppTheme.light(),
materialTheme: AppTheme.materialLight(),
home: AppComponentThemes(
child: Builder(
builder: (context) {
textFieldTheme = shad.ComponentTheme.of<shad.TextFieldTheme>(context);
return const SizedBox();
},
),
),
),
);
expect(textFieldTheme.filled, isTrue);
expect(textFieldTheme.border, Border.all(color: AppPalette.light.border));
expect(textFieldTheme.borderRadius, BorderRadius.circular(AppDesignTokens.controlRadius));
expect(textFieldTheme.padding, AppDesignTokens.textFieldPadding);
});
testWidgets('provides the standard checkbox metrics', (tester) async {
late shad.CheckboxTheme checkboxTheme;
+3 -1
View File
@@ -2336,7 +2336,9 @@ void main() {
await tester.pumpAndSettle();
final createDirectoryInput = find.byKey(const ValueKey('create-task-directory-input'));
final createRenameInput = find.byKey(const ValueKey('create-task-rename-input'));
final createConnectionsInput = find.byKey(const ValueKey('create-task-connections-input'));
expect(tester.getSize(createDirectoryInput).height, tester.getSize(createRenameInput).height);
expect(tester.getSize(createConnectionsInput), tester.getSize(createRenameInput));
final createDirectoryField = tester.widget<shad.TextField>(createDirectoryInput);
final createRenameField = tester.widget<shad.TextField>(
find.descendant(of: createRenameInput, matching: find.byType(AppTextField)),
@@ -4875,7 +4877,7 @@ class _CreateTaskPageHarness extends StatelessWidget {
return shad.ShadcnApp(
theme: AppTheme.light(),
materialTheme: AppTheme.materialLight(),
home: const CreateTaskWindowPage(),
home: const AppComponentThemes(child: CreateTaskWindowPage()),
);
}
}