refactor(apps): 主题系统迁移至 ColorScheme + 扩展架构并支持 Dark Mode
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum CalendarViewType { day, month }
|
||||
|
||||
class CalendarState {
|
||||
final CalendarViewType viewType;
|
||||
final DateTime selectedDate;
|
||||
|
||||
CalendarState({required this.viewType, required this.selectedDate});
|
||||
|
||||
CalendarState copyWith({CalendarViewType? viewType, DateTime? selectedDate}) {
|
||||
return CalendarState(
|
||||
viewType: viewType ?? this.viewType,
|
||||
selectedDate: selectedDate ?? this.selectedDate,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CalendarStateManager extends ChangeNotifier {
|
||||
CalendarState _state;
|
||||
|
||||
CalendarStateManager()
|
||||
: _state = CalendarState(
|
||||
viewType: CalendarViewType.month,
|
||||
selectedDate: DateTime.now(),
|
||||
);
|
||||
|
||||
CalendarState get state => _state;
|
||||
|
||||
CalendarViewType get viewType => _state.viewType;
|
||||
DateTime get selectedDate => _state.selectedDate;
|
||||
|
||||
void setViewType(CalendarViewType type) {
|
||||
_state = _state.copyWith(viewType: type);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setSelectedDate(DateTime date) {
|
||||
_state = _state.copyWith(selectedDate: date);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void resetToToday() {
|
||||
final now = DateTime.now();
|
||||
_state = CalendarState(
|
||||
viewType: CalendarViewType.month,
|
||||
selectedDate: DateTime(now.year, now.month, now.day),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ class AppButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isDisabled = onPressed == null || isLoading;
|
||||
|
||||
if (isOutlined) {
|
||||
@@ -30,17 +31,15 @@ class AppButton extends StatelessWidget {
|
||||
onPressed: isLoading ? null : onPressed,
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: isDisabled
|
||||
? AppColors.authSecondaryButtonBackground.withValues(
|
||||
alpha: 0.55,
|
||||
)
|
||||
: AppColors.authSecondaryButtonBackground,
|
||||
? colorScheme.secondaryContainer.withValues(alpha: 0.55)
|
||||
: colorScheme.secondaryContainer,
|
||||
foregroundColor: isDisabled
|
||||
? AppColors.authLinkMuted
|
||||
: AppColors.authSecondaryButtonText,
|
||||
? colorScheme.outline
|
||||
: colorScheme.onSecondaryContainer,
|
||||
side: BorderSide(
|
||||
color: isDisabled
|
||||
? AppColors.authSecondaryButtonBorder.withValues(alpha: 0.7)
|
||||
: AppColors.authSecondaryButtonBorder,
|
||||
? colorScheme.outlineVariant.withValues(alpha: 0.7)
|
||||
: colorScheme.outlineVariant,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
@@ -48,10 +47,10 @@ class AppButton extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xl),
|
||||
),
|
||||
child: isLoading
|
||||
? const AppLoadingIndicator(
|
||||
? AppLoadingIndicator(
|
||||
variant: AppLoadingVariant.button,
|
||||
color: AppColors.authSecondaryButtonText,
|
||||
trackColor: AppColors.authSecondaryButtonBorder,
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
trackColor: colorScheme.outlineVariant,
|
||||
)
|
||||
: Text(
|
||||
text,
|
||||
@@ -72,7 +71,7 @@ class AppButton extends StatelessWidget {
|
||||
? const []
|
||||
: [
|
||||
BoxShadow(
|
||||
color: AppColors.blue300.withValues(alpha: 0.24),
|
||||
color: colorScheme.primary.withValues(alpha: 0.24),
|
||||
blurRadius: 18,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
@@ -87,19 +86,17 @@ class AppButton extends StatelessWidget {
|
||||
elevation: const WidgetStatePropertyAll(0),
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.disabled)) {
|
||||
return AppColors.authPrimaryButtonDisabled;
|
||||
return colorScheme.surfaceContainerHighest;
|
||||
}
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return AppColors.authPrimaryButtonPressed;
|
||||
return colorScheme.primary.withValues(alpha: 0.85);
|
||||
}
|
||||
return AppColors.authPrimaryButton;
|
||||
return colorScheme.primary;
|
||||
}),
|
||||
foregroundColor: const WidgetStatePropertyAll(
|
||||
AppColors.authPrimaryButtonText,
|
||||
),
|
||||
foregroundColor: WidgetStatePropertyAll(colorScheme.onPrimary),
|
||||
overlayColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return AppColors.white.withValues(alpha: 0.08);
|
||||
return colorScheme.onPrimary.withValues(alpha: 0.08);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
@@ -113,10 +110,10 @@ class AppButton extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: isLoading
|
||||
? const AppLoadingIndicator(
|
||||
? AppLoadingIndicator(
|
||||
variant: AppLoadingVariant.button,
|
||||
color: AppColors.authPrimaryButtonText,
|
||||
trackColor: AppColors.blue400,
|
||||
color: colorScheme.onPrimary,
|
||||
trackColor: colorScheme.primaryContainer,
|
||||
)
|
||||
: Text(
|
||||
text,
|
||||
@@ -125,8 +122,8 @@ class AppButton extends StatelessWidget {
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.2,
|
||||
color: isDisabled
|
||||
? AppColors.authLinkMuted
|
||||
: AppColors.authPrimaryButtonText,
|
||||
? colorScheme.outline
|
||||
: colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/theme/design_tokens.dart';
|
||||
|
||||
class AppInput extends StatelessWidget {
|
||||
final String label;
|
||||
@@ -25,15 +24,16 @@ class AppInput extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.slate600,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
@@ -40,49 +40,46 @@ class AppLoadingIndicator extends StatelessWidget {
|
||||
};
|
||||
}
|
||||
|
||||
Color get _resolvedColor {
|
||||
return color ??
|
||||
switch (variant) {
|
||||
AppLoadingVariant.surface => AppColors.blue500,
|
||||
AppLoadingVariant.inline => AppColors.slate500,
|
||||
AppLoadingVariant.button => AppColors.white,
|
||||
};
|
||||
}
|
||||
|
||||
Color get _resolvedTrackColor {
|
||||
return trackColor ??
|
||||
switch (variant) {
|
||||
AppLoadingVariant.surface => AppColors.blue100,
|
||||
AppLoadingVariant.inline => AppColors.slate200,
|
||||
AppLoadingVariant.button => AppColors.blue300,
|
||||
};
|
||||
}
|
||||
|
||||
bool get _resolvedWithContainer {
|
||||
return withContainer ??
|
||||
switch (variant) {
|
||||
AppLoadingVariant.surface => true,
|
||||
AppLoadingVariant.inline => false,
|
||||
AppLoadingVariant.button => false,
|
||||
};
|
||||
}
|
||||
|
||||
Widget _buildSpinner() {
|
||||
Widget _buildSpinner(Color color, Color trackColor) {
|
||||
return SizedBox(
|
||||
width: _resolvedSize,
|
||||
height: _resolvedSize,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: _resolvedStrokeWidth,
|
||||
color: _resolvedColor,
|
||||
backgroundColor: _resolvedTrackColor,
|
||||
color: color,
|
||||
backgroundColor: trackColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_resolvedWithContainer) {
|
||||
return _buildSpinner();
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final resolvedColor =
|
||||
color ??
|
||||
switch (variant) {
|
||||
AppLoadingVariant.surface => colorScheme.primary,
|
||||
AppLoadingVariant.inline => colorScheme.onSurfaceVariant,
|
||||
AppLoadingVariant.button => colorScheme.onPrimary,
|
||||
};
|
||||
|
||||
final resolvedTrackColor =
|
||||
trackColor ??
|
||||
switch (variant) {
|
||||
AppLoadingVariant.surface => colorScheme.primaryContainer,
|
||||
AppLoadingVariant.inline => colorScheme.outlineVariant,
|
||||
AppLoadingVariant.button => colorScheme.secondary,
|
||||
};
|
||||
|
||||
if (withContainer == false ||
|
||||
(withContainer == null &&
|
||||
switch (variant) {
|
||||
AppLoadingVariant.surface => true,
|
||||
AppLoadingVariant.inline => false,
|
||||
AppLoadingVariant.button => false,
|
||||
})) {
|
||||
return _buildSpinner(resolvedColor, resolvedTrackColor);
|
||||
}
|
||||
|
||||
return Container(
|
||||
@@ -90,18 +87,18 @@ class AppLoadingIndicator extends StatelessWidget {
|
||||
height: _resolvedSize + AppSpacing.md,
|
||||
padding: const EdgeInsets.all(AppSpacing.xs),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.slate200.withValues(alpha: 0.55),
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.55),
|
||||
blurRadius: AppRadius.md,
|
||||
offset: const Offset(0, AppSpacing.xs),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: _buildSpinner(),
|
||||
child: _buildSpinner(resolvedColor, resolvedTrackColor),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/theme/design_tokens.dart';
|
||||
|
||||
class AppPressable extends StatefulWidget {
|
||||
const AppPressable({
|
||||
super.key,
|
||||
@@ -25,25 +23,22 @@ class _AppPressableState extends State<AppPressable> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return AnimatedScale(
|
||||
scale: _isPressed ? widget.pressedScale : 1,
|
||||
duration: const Duration(milliseconds: 110),
|
||||
curve: Curves.easeOut,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
color: colorScheme.surface.withValues(alpha: 0),
|
||||
child: InkWell(
|
||||
borderRadius: widget.borderRadius,
|
||||
onTap: widget.onTap,
|
||||
onHighlightChanged: (pressed) {
|
||||
if (_isPressed == pressed) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isPressed = pressed;
|
||||
});
|
||||
if (_isPressed == pressed) return;
|
||||
setState(() => _isPressed = pressed);
|
||||
},
|
||||
splashColor: AppColors.blue100.withValues(alpha: 0.32),
|
||||
highlightColor: AppColors.blue50.withValues(alpha: 0.28),
|
||||
splashColor: colorScheme.primary.withValues(alpha: 0.12),
|
||||
highlightColor: colorScheme.primary.withValues(alpha: 0.08),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -18,6 +18,7 @@ class AppPullRefreshFeedback extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final resolvedLabel = label ?? context.l10n.commonRefreshing;
|
||||
return IgnorePointer(
|
||||
child: AnimatedOpacity(
|
||||
@@ -31,24 +32,24 @@ class AppPullRefreshFeedback extends StatelessWidget {
|
||||
vertical: AppSpacing.xs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const AppLoadingIndicator(
|
||||
AppLoadingIndicator(
|
||||
variant: AppLoadingVariant.inline,
|
||||
color: AppColors.blue500,
|
||||
trackColor: AppColors.blue100,
|
||||
color: colorScheme.primary,
|
||||
trackColor: colorScheme.primaryContainer,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Text(
|
||||
resolvedLabel,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: AppColors.slate600,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -20,8 +20,9 @@ Future<T?> showAppSelectionSheet<T>(
|
||||
final result = await showModalBottomSheet<T>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface.withValues(alpha: 0),
|
||||
builder: (sheetContext) {
|
||||
final colorScheme = Theme.of(sheetContext).colorScheme;
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Container(
|
||||
@@ -33,9 +34,9 @@ Future<T?> showAppSelectionSheet<T>(
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -46,10 +47,10 @@ Future<T?> showAppSelectionSheet<T>(
|
||||
child: Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -62,9 +63,9 @@ Future<T?> showAppSelectionSheet<T>(
|
||||
isSelected: isSelected,
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
const Divider(height: 1, color: AppColors.border),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
Divider(height: 1, color: colorScheme.outlineVariant),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg),
|
||||
child: SizedBox(
|
||||
@@ -90,6 +91,7 @@ Widget _buildItem<T>(
|
||||
required AppSelectionItem<T> item,
|
||||
required bool isSelected,
|
||||
}) {
|
||||
final colorScheme = Theme.of(sheetContext).colorScheme;
|
||||
return InkWell(
|
||||
onTap: () => Navigator.of(sheetContext).pop(item.value),
|
||||
child: Container(
|
||||
@@ -105,12 +107,14 @@ Widget _buildItem<T>(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||
color: isSelected ? AppColors.blue600 : AppColors.slate800,
|
||||
color: isSelected
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isSelected)
|
||||
const Icon(Icons.check, size: 20, color: AppColors.blue600),
|
||||
Icon(Icons.check, size: 20, color: colorScheme.primary),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -50,16 +50,17 @@ class _AppSheetInputFieldState extends State<AppSheetInputField> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.label,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate700,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
@@ -67,17 +68,17 @@ class _AppSheetInputFieldState extends State<AppSheetInputField> {
|
||||
duration: const Duration(milliseconds: 140),
|
||||
curve: Curves.easeOut,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.slate50,
|
||||
color: colorScheme.surfaceContainerLowest,
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
border: Border.all(
|
||||
color: _isFocused
|
||||
? AppColors.borderQuaternary
|
||||
: AppColors.borderSecondary,
|
||||
? colorScheme.primary
|
||||
: colorScheme.outlineVariant,
|
||||
),
|
||||
boxShadow: _isFocused
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColors.blue200.withValues(alpha: 0.35),
|
||||
color: colorScheme.secondary.withValues(alpha: 0.35),
|
||||
blurRadius: AppRadius.sm,
|
||||
offset: const Offset(0, AppSpacing.xs / 2),
|
||||
),
|
||||
@@ -91,7 +92,9 @@ class _AppSheetInputFieldState extends State<AppSheetInputField> {
|
||||
maxLines: widget.maxLines,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hint,
|
||||
hintStyle: const TextStyle(color: AppColors.slate400),
|
||||
hintStyle: TextStyle(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
|
||||
@@ -22,6 +22,7 @@ class AppToggleSwitch extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onTap: onChanged == null ? null : () => onChanged!(!value),
|
||||
child: Opacity(
|
||||
@@ -32,13 +33,13 @@ class AppToggleSwitch extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(AppSpacing.xs / 2),
|
||||
decoration: BoxDecoration(
|
||||
color: value
|
||||
? (activeBackgroundColor ?? AppColors.blue100)
|
||||
: (inactiveBackgroundColor ?? AppColors.surfaceTertiary),
|
||||
? (activeBackgroundColor ?? colorScheme.primaryContainer)
|
||||
: (inactiveBackgroundColor ?? colorScheme.tertiaryContainer),
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
border: Border.all(
|
||||
color: value
|
||||
? (activeBorderColor ?? AppColors.blue300)
|
||||
: (inactiveBorderColor ?? AppColors.borderSecondary),
|
||||
? (activeBorderColor ?? colorScheme.tertiary)
|
||||
: (inactiveBorderColor ?? colorScheme.outlineVariant),
|
||||
),
|
||||
),
|
||||
child: AnimatedAlign(
|
||||
@@ -48,9 +49,9 @@ class AppToggleSwitch extends StatelessWidget {
|
||||
width: AppSpacing.lg + AppSpacing.xs,
|
||||
height: AppSpacing.lg + AppSpacing.xs,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -21,6 +21,7 @@ class BackTitlePageHeader extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: Stack(
|
||||
@@ -50,10 +51,10 @@ class BackTitlePageHeader extends StatelessWidget {
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
import '../../core/theme/design_tokens.dart';
|
||||
|
||||
enum DockTab { todo, calendar }
|
||||
|
||||
class BottomDock extends StatelessWidget {
|
||||
final DockTab activeTab;
|
||||
final VoidCallback? onTodoTap;
|
||||
final VoidCallback? onCalendarTap;
|
||||
final VoidCallback? onHomeTap;
|
||||
|
||||
const BottomDock({
|
||||
super.key,
|
||||
required this.activeTab,
|
||||
this.onTodoTap,
|
||||
this.onCalendarTap,
|
||||
this.onHomeTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
height: 72,
|
||||
padding: const EdgeInsets.only(
|
||||
left: AppSpacing.xl,
|
||||
right: AppSpacing.xl,
|
||||
top: AppSpacing.md,
|
||||
bottom: AppSpacing.sm,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildToggle(context, colorScheme),
|
||||
_buildHomeBtn(context, colorScheme),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToggle(BuildContext context, ColorScheme colorScheme) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xxl),
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.shadow.withValues(alpha: 0.12),
|
||||
blurRadius: AppRadius.sm,
|
||||
offset: const Offset(0, AppSpacing.xs / 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildToggleItem(
|
||||
icon: LucideIcons.listTodo,
|
||||
isActive: activeTab == DockTab.todo,
|
||||
onTap: onTodoTap,
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_buildToggleItem(
|
||||
icon: LucideIcons.calendar,
|
||||
isActive: activeTab == DockTab.calendar,
|
||||
onTap: onCalendarTap,
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToggleItem({
|
||||
required IconData icon,
|
||||
required bool isActive,
|
||||
required ColorScheme colorScheme,
|
||||
VoidCallback? onTap,
|
||||
}) {
|
||||
return Material(
|
||||
color: colorScheme.surface.withValues(alpha: 0),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 140),
|
||||
curve: Curves.easeOut,
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive
|
||||
? colorScheme.secondaryContainer
|
||||
: colorScheme.surface.withValues(alpha: 0),
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(
|
||||
color: isActive
|
||||
? colorScheme.primary.withValues(alpha: 0.35)
|
||||
: colorScheme.surface.withValues(alpha: 0),
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: isActive
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHomeBtn(BuildContext context, ColorScheme colorScheme) {
|
||||
return Material(
|
||||
color: colorScheme.surface.withValues(alpha: 0),
|
||||
child: InkWell(
|
||||
key: const ValueKey('bottom_dock_home_button'),
|
||||
onTap: onHomeTap,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
child: Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.shadow.withValues(alpha: 0.12),
|
||||
blurRadius: AppRadius.sm,
|
||||
offset: const Offset(0, AppSpacing.xs / 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.home,
|
||||
size: 20,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/l10n/l10n.dart';
|
||||
import '../../../core/theme/design_tokens.dart';
|
||||
|
||||
enum MessageSender { user, ai }
|
||||
|
||||
@@ -23,6 +22,7 @@ class ChatBubble extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isUser = sender == MessageSender.user;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
@@ -36,7 +36,10 @@ class ChatBubble extends StatelessWidget {
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Text(
|
||||
_formatTimestamp(timestamp),
|
||||
style: const TextStyle(fontSize: 11, color: AppColors.slate400),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
@@ -45,11 +48,11 @@ class ChatBubble extends StatelessWidget {
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isUser ? AppColors.blue500 : AppColors.white,
|
||||
color: isUser ? colorScheme.primary : colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.06),
|
||||
color: colorScheme.shadow.withValues(alpha: 0.08),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -60,7 +63,9 @@ class ChatBubble extends StatelessWidget {
|
||||
content,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: isUser ? AppColors.white : AppColors.slate700,
|
||||
color: isUser
|
||||
? colorScheme.onPrimary
|
||||
: colorScheme.onSurface,
|
||||
height: 1.45,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -18,8 +18,10 @@ Future<bool> showConfirmSheet(
|
||||
final result = await showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface.withValues(alpha: 0),
|
||||
builder: (sheetContext) {
|
||||
final colorScheme = Theme.of(sheetContext).colorScheme;
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Container(
|
||||
@@ -31,9 +33,9 @@ Future<bool> showConfirmSheet(
|
||||
),
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -42,17 +44,20 @@ Future<bool> showConfirmSheet(
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 14, color: AppColors.slate500),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
SizedBox(
|
||||
@@ -63,16 +68,18 @@ Future<bool> showConfirmSheet(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: isDestructive
|
||||
? AppColors.feedbackErrorIcon
|
||||
: AppColors.blue600,
|
||||
? colorScheme.error
|
||||
: colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
),
|
||||
child: Text(
|
||||
resolvedConfirmText,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.white,
|
||||
color: isDestructive
|
||||
? colorScheme.onError
|
||||
: colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -13,8 +13,10 @@ Future<bool> showDestructiveActionSheet(
|
||||
final result = await showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface.withValues(alpha: 0),
|
||||
builder: (sheetContext) {
|
||||
final colorScheme = Theme.of(sheetContext).colorScheme;
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Container(
|
||||
@@ -26,9 +28,9 @@ Future<bool> showDestructiveActionSheet(
|
||||
),
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -37,17 +39,20 @@ Future<bool> showDestructiveActionSheet(
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 14, color: AppColors.slate500),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
SizedBox(
|
||||
@@ -57,15 +62,15 @@ Future<bool> showDestructiveActionSheet(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.feedbackErrorIcon,
|
||||
color: colorScheme.error,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
),
|
||||
child: Text(
|
||||
confirmText,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.white,
|
||||
color: colorScheme.onError,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -72,7 +72,7 @@ class _DetailHeaderActionMenuState<T> extends State<DetailHeaderActionMenu<T>> {
|
||||
CompositedTransformFollower(
|
||||
link: _layerLink,
|
||||
showWhenUnlinked: false,
|
||||
offset: const Offset(
|
||||
offset: Offset(
|
||||
_buttonSize - _menuWidth,
|
||||
_buttonSize + AppSpacing.sm,
|
||||
),
|
||||
@@ -101,18 +101,19 @@ class _DetailHeaderActionMenuState<T> extends State<DetailHeaderActionMenu<T>> {
|
||||
}
|
||||
|
||||
Widget _buildMenuCard() {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
color: colorScheme.surface.withValues(alpha: 0),
|
||||
child: Container(
|
||||
width: _menuWidth,
|
||||
padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.slate300.withValues(alpha: 0.42),
|
||||
color: colorScheme.shadow.withValues(alpha: 0.42),
|
||||
blurRadius: AppSpacing.xl,
|
||||
offset: const Offset(0, AppSpacing.sm),
|
||||
),
|
||||
@@ -125,12 +126,14 @@ class _DetailHeaderActionMenuState<T> extends State<DetailHeaderActionMenu<T>> {
|
||||
for (int i = 0; i < widget.items.length; i++) ...[
|
||||
_buildMenuItem(widget.items[i]),
|
||||
if (i < widget.items.length - 1)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
),
|
||||
child: Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: AppColors.slate100,
|
||||
color: colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -141,21 +144,26 @@ class _DetailHeaderActionMenuState<T> extends State<DetailHeaderActionMenu<T>> {
|
||||
}
|
||||
|
||||
Widget _buildMenuItem(DetailHeaderActionItem<T> item) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final textColor = item.isDestructive
|
||||
? AppColors.red500
|
||||
: (item.enabled ? AppColors.slate700 : AppColors.slate400);
|
||||
? colorScheme.error
|
||||
: (item.enabled ? colorScheme.onSurface : colorScheme.onSurfaceVariant);
|
||||
final pressedColor = item.isDestructive
|
||||
? AppColors.feedbackErrorSurface
|
||||
: AppColors.surfaceInfoLight;
|
||||
? colorScheme.errorContainer
|
||||
: colorScheme.primaryContainer;
|
||||
|
||||
return SizedBox(
|
||||
height: AppSpacing.xxl * 2,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
color: colorScheme.surface.withValues(alpha: 0),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
splashColor: item.enabled ? pressedColor : Colors.transparent,
|
||||
highlightColor: item.enabled ? pressedColor : Colors.transparent,
|
||||
splashColor: item.enabled
|
||||
? pressedColor
|
||||
: colorScheme.surface.withValues(alpha: 0),
|
||||
highlightColor: item.enabled
|
||||
? pressedColor
|
||||
: colorScheme.surface.withValues(alpha: 0),
|
||||
onTap: item.enabled ? () => _handleSelect(item.value) : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
@@ -190,6 +198,7 @@ class _DetailHeaderActionMenuState<T> extends State<DetailHeaderActionMenu<T>> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return CompositedTransformTarget(
|
||||
link: _layerLink,
|
||||
child: GestureDetector(
|
||||
@@ -200,19 +209,19 @@ class _DetailHeaderActionMenuState<T> extends State<DetailHeaderActionMenu<T>> {
|
||||
height: _buttonSize,
|
||||
decoration: BoxDecoration(
|
||||
color: _isMenuOpen
|
||||
? AppColors.surfaceInfo
|
||||
: AppColors.surfaceTertiary,
|
||||
? colorScheme.secondaryContainer
|
||||
: colorScheme.tertiaryContainer,
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
border: Border.all(
|
||||
color: _isMenuOpen
|
||||
? AppColors.borderQuaternary
|
||||
: AppColors.borderTertiary,
|
||||
? colorScheme.outlineVariant
|
||||
: colorScheme.outline,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
Icons.more_horiz,
|
||||
size: AppSpacing.lg + AppSpacing.xs,
|
||||
color: AppColors.slate600,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -18,6 +18,8 @@ class ErrorRetrySurface extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
|
||||
@@ -28,7 +30,7 @@ class ErrorRetrySurface extends StatelessWidget {
|
||||
Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: AppColors.red500),
|
||||
style: TextStyle(color: colorScheme.error),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
AppButton(text: context.l10n.commonRetry, onPressed: onRetry),
|
||||
|
||||
@@ -99,6 +99,7 @@ class _FixedLengthCodeInputState extends State<FixedLengthCodeInput> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final chars = widget.controller.text.split('');
|
||||
final slotHeight = AppSpacing.xl * 2 + AppSpacing.sm;
|
||||
final slotSpacing = AppSpacing.sm;
|
||||
@@ -113,17 +114,17 @@ class _FixedLengthCodeInputState extends State<FixedLengthCodeInput> {
|
||||
duration: const Duration(milliseconds: 180),
|
||||
padding: const EdgeInsets.all(AppSpacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.authSectionBackground,
|
||||
color: colorScheme.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(
|
||||
color: _isFocused
|
||||
? AppColors.authInputFocus
|
||||
: AppColors.authSectionBorder,
|
||||
? colorScheme.primary
|
||||
: colorScheme.outlineVariant,
|
||||
),
|
||||
boxShadow: _isFocused
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColors.blue200.withValues(alpha: 0.28),
|
||||
color: colorScheme.secondary.withValues(alpha: 0.28),
|
||||
blurRadius: 18,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
@@ -164,6 +165,7 @@ class _FixedLengthCodeInputState extends State<FixedLengthCodeInput> {
|
||||
chars: chars,
|
||||
slotHeight: slotHeight,
|
||||
isComplete: isComplete,
|
||||
colorScheme: colorScheme,
|
||||
),
|
||||
),
|
||||
if (index != widget.length - 1)
|
||||
@@ -185,6 +187,7 @@ class _FixedLengthCodeInputState extends State<FixedLengthCodeInput> {
|
||||
required List<String> chars,
|
||||
required double slotHeight,
|
||||
required bool isComplete,
|
||||
required ColorScheme colorScheme,
|
||||
}) {
|
||||
final hasChar = index < chars.length;
|
||||
final isActive =
|
||||
@@ -195,19 +198,19 @@ class _FixedLengthCodeInputState extends State<FixedLengthCodeInput> {
|
||||
height: slotHeight,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: hasChar ? AppColors.white : AppColors.authInputBackground,
|
||||
color: hasChar ? colorScheme.surface : colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
border: Border.all(
|
||||
color: isActive
|
||||
? AppColors.authPrimaryButton
|
||||
? colorScheme.primary
|
||||
: isComplete
|
||||
? AppColors.authSecondaryButtonBorder
|
||||
: AppColors.authInputBorder,
|
||||
? colorScheme.outlineVariant
|
||||
: colorScheme.outline,
|
||||
),
|
||||
boxShadow: isActive
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColors.blue200.withValues(alpha: 0.32),
|
||||
color: colorScheme.secondary.withValues(alpha: 0.32),
|
||||
blurRadius: 14,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
@@ -219,7 +222,7 @@ class _FixedLengthCodeInputState extends State<FixedLengthCodeInput> {
|
||||
style: TextStyle(
|
||||
fontSize: AppSpacing.xl,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: hasChar ? AppColors.slate900 : AppColors.authLinkMuted,
|
||||
color: hasChar ? colorScheme.onSurface : colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -20,9 +20,10 @@ class LinkButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final color = enabled
|
||||
? (foregroundColor ?? AppColors.authLinkText)
|
||||
: AppColors.slate300;
|
||||
? (foregroundColor ?? colorScheme.primary)
|
||||
: colorScheme.outline;
|
||||
|
||||
return TextButton(
|
||||
onPressed: enabled ? onTap : null,
|
||||
|
||||
@@ -73,6 +73,7 @@ class MessageComposer extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return KeyedSubtree(
|
||||
key: messageComposerContainerKey,
|
||||
child: Container(
|
||||
@@ -82,19 +83,19 @@ class MessageComposer extends StatelessWidget {
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.homeComposerShell,
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.xxl),
|
||||
border: Border.all(color: AppColors.homeComposerBorder),
|
||||
boxShadow: const [
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.slate200,
|
||||
color: colorScheme.surfaceContainerHighest.withValues(alpha: 1),
|
||||
blurRadius: AppRadius.lg,
|
||||
offset: Offset(AppSpacing.none, AppSpacing.sm),
|
||||
offset: const Offset(AppSpacing.none, AppSpacing.sm),
|
||||
),
|
||||
BoxShadow(
|
||||
color: AppColors.white,
|
||||
color: colorScheme.surface,
|
||||
blurRadius: AppRadius.md,
|
||||
offset: Offset(AppSpacing.none, -AppSpacing.xs),
|
||||
offset: const Offset(AppSpacing.none, -AppSpacing.xs),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -114,30 +115,30 @@ class MessageComposer extends StatelessWidget {
|
||||
icon: Icon(
|
||||
LucideIcons.plus,
|
||||
size: iconSize,
|
||||
color: AppColors.slate500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(child: _buildCenterArea()),
|
||||
Expanded(child: _buildCenterArea(colorScheme)),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
IconButton(
|
||||
key: messageComposerRightButtonKey,
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: onTapRightAction,
|
||||
icon: _isTranscribing
|
||||
? const AppLoadingIndicator(
|
||||
? AppLoadingIndicator(
|
||||
variant: AppLoadingVariant.inline,
|
||||
size: AppSpacing.lg,
|
||||
strokeWidth: AppSpacing.xs / 2,
|
||||
color: AppColors.blue600,
|
||||
trackColor: AppColors.blue100,
|
||||
color: colorScheme.primary,
|
||||
trackColor: colorScheme.primaryContainer,
|
||||
)
|
||||
: Icon(
|
||||
_resolveRightIcon(),
|
||||
size: iconSize,
|
||||
color: _resolveRightIconColor(),
|
||||
color: _resolveRightIconColor(colorScheme),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -147,7 +148,7 @@ class MessageComposer extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCenterArea() {
|
||||
Widget _buildCenterArea(ColorScheme colorScheme) {
|
||||
return SizedBox(
|
||||
height: composerMinHeight,
|
||||
child: AnimatedSwitcher(
|
||||
@@ -155,7 +156,10 @@ class MessageComposer extends StatelessWidget {
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeOut,
|
||||
child: _isHoldMode
|
||||
? _buildHoldToSpeakArea(key: const ValueKey('hold_mode'))
|
||||
? _buildHoldToSpeakArea(
|
||||
key: const ValueKey('hold_mode'),
|
||||
colorScheme: colorScheme,
|
||||
)
|
||||
: _buildTextInputArea(key: const ValueKey('text_mode')),
|
||||
),
|
||||
);
|
||||
@@ -165,7 +169,10 @@ class MessageComposer extends StatelessWidget {
|
||||
return SizedBox(key: key, height: composerMinHeight, child: textInputChild);
|
||||
}
|
||||
|
||||
Widget _buildHoldToSpeakArea({required Key key}) {
|
||||
Widget _buildHoldToSpeakArea({
|
||||
required Key key,
|
||||
required ColorScheme colorScheme,
|
||||
}) {
|
||||
return RawGestureDetector(
|
||||
key: messageComposerHoldAreaKey,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
@@ -176,8 +183,8 @@ class MessageComposer extends StatelessWidget {
|
||||
duration: const Duration(milliseconds: _holdActivateDurationMs),
|
||||
),
|
||||
(instance) {
|
||||
instance.onLongPressStart = (_) => onHoldToSpeakStart();
|
||||
instance.onLongPressEnd = (_) => onHoldToSpeakEnd();
|
||||
instance.onLongPressStart = (details) => onHoldToSpeakStart();
|
||||
instance.onLongPressEnd = (details) => onHoldToSpeakEnd();
|
||||
instance.onLongPressMoveUpdate = onHoldToSpeakMoveUpdate;
|
||||
instance.onLongPressCancel = onHoldToSpeakCancel;
|
||||
},
|
||||
@@ -188,12 +195,12 @@ class MessageComposer extends StatelessWidget {
|
||||
width: double.infinity,
|
||||
height: composerMinHeight,
|
||||
alignment: Alignment.center,
|
||||
child: _buildHoldToSpeakContent(),
|
||||
child: _buildHoldToSpeakContent(colorScheme),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHoldToSpeakContent() {
|
||||
Widget _buildHoldToSpeakContent(ColorScheme colorScheme) {
|
||||
final l10n = L10n.current;
|
||||
final resolvedRecordingText =
|
||||
recordingText ?? l10n.homeRecordingReleaseSend;
|
||||
@@ -208,7 +215,7 @@ class MessageComposer extends StatelessWidget {
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
resolvedRecordingText,
|
||||
style: const TextStyle(color: AppColors.slate700),
|
||||
style: TextStyle(color: colorScheme.onSurface),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -220,13 +227,13 @@ class MessageComposer extends StatelessWidget {
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
resolvedRecordingText,
|
||||
style: const TextStyle(color: AppColors.slate700),
|
||||
style: TextStyle(color: colorScheme.onSurface),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
resolvedRecordingHintText,
|
||||
key: messageComposerRecordingHintKey,
|
||||
style: const TextStyle(color: AppColors.slate500),
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -237,7 +244,7 @@ class MessageComposer extends StatelessWidget {
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
resolvedTranscribingText,
|
||||
style: const TextStyle(color: AppColors.slate500),
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -246,7 +253,7 @@ class MessageComposer extends StatelessWidget {
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
resolvedHoldToSpeakText,
|
||||
style: const TextStyle(color: AppColors.slate500),
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -261,10 +268,10 @@ class MessageComposer extends StatelessWidget {
|
||||
return _isHoldMode ? LucideIcons.keyboard : LucideIcons.mic;
|
||||
}
|
||||
|
||||
Color _resolveRightIconColor() {
|
||||
Color _resolveRightIconColor(ColorScheme colorScheme) {
|
||||
if (isWaitingAgent || hasMessage) {
|
||||
return AppColors.blue600;
|
||||
return colorScheme.primary;
|
||||
}
|
||||
return AppColors.slate500;
|
||||
return colorScheme.onSurfaceVariant;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,10 +42,13 @@ class _BackButtonState extends State<BackButton> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final background = _isPressed ? AppColors.surfaceInfo : AppColors.white;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final background = _isPressed
|
||||
? colorScheme.secondaryContainer
|
||||
: colorScheme.surface;
|
||||
final borderColor = _isPressed
|
||||
? AppColors.borderQuaternary
|
||||
: AppColors.borderTertiary;
|
||||
? colorScheme.outlineVariant
|
||||
: colorScheme.outline;
|
||||
|
||||
return AnimatedScale(
|
||||
scale: _isPressed ? 0.96 : 1,
|
||||
@@ -60,13 +63,13 @@ class _BackButtonState extends State<BackButton> {
|
||||
boxShadow: _isPressed
|
||||
? const []
|
||||
: [
|
||||
const BoxShadow(
|
||||
color: AppColors.white,
|
||||
BoxShadow(
|
||||
color: colorScheme.surface,
|
||||
blurRadius: AppRadius.sm,
|
||||
offset: Offset(0, -1),
|
||||
offset: const Offset(0, -1),
|
||||
),
|
||||
BoxShadow(
|
||||
color: AppColors.slate200.withValues(alpha: 0.42),
|
||||
color: colorScheme.shadow.withValues(alpha: 0.42),
|
||||
blurRadius: AppRadius.md,
|
||||
offset: const Offset(0, AppSpacing.xs),
|
||||
),
|
||||
@@ -76,39 +79,29 @@ class _BackButtonState extends State<BackButton> {
|
||||
width: AppSpacing.xl * 2,
|
||||
height: AppSpacing.xl * 2,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
color: colorScheme.surface.withValues(alpha: 0),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
onTap: widget.onPressed ?? () => Navigator.of(context).pop(),
|
||||
onTapDown: (_) {
|
||||
if (_isPressed) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isPressed = true;
|
||||
});
|
||||
if (_isPressed) return;
|
||||
setState(() => _isPressed = true);
|
||||
},
|
||||
onTapCancel: () {
|
||||
if (!_isPressed) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isPressed = false;
|
||||
});
|
||||
if (!_isPressed) return;
|
||||
setState(() => _isPressed = false);
|
||||
},
|
||||
onTapUp: (_) {
|
||||
if (!_isPressed) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isPressed = false;
|
||||
});
|
||||
if (!_isPressed) return;
|
||||
setState(() => _isPressed = false);
|
||||
},
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.chevron_left,
|
||||
size: AppSpacing.lg + AppSpacing.xs,
|
||||
color: _isPressed ? AppColors.blue600 : AppColors.slate700,
|
||||
color: _isPressed
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/theme/design_tokens.dart';
|
||||
|
||||
class PhonePrefixSelector extends StatelessWidget {
|
||||
const PhonePrefixSelector({
|
||||
@@ -15,8 +14,9 @@ class PhonePrefixSelector extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: AppSpacing.md, right: AppSpacing.sm),
|
||||
padding: const EdgeInsets.only(left: 12, right: 8),
|
||||
child: PopupMenuButton<String>(
|
||||
onSelected: onChanged,
|
||||
itemBuilder: (context) => items
|
||||
@@ -24,26 +24,24 @@ class PhonePrefixSelector extends StatelessWidget {
|
||||
(item) => PopupMenuItem<String>(value: item, child: Text(item)),
|
||||
)
|
||||
.toList(growable: false),
|
||||
color: AppColors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
),
|
||||
color: colorScheme.surface,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.slate700,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
const Icon(
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.arrow_drop_down,
|
||||
size: 18,
|
||||
color: AppColors.slate500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -92,6 +92,7 @@ class _ToastWidgetState extends State<_ToastWidget>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final config = ToastTypeConfig.fromType(context, widget.type);
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 12,
|
||||
@@ -115,7 +116,7 @@ class _ToastWidgetState extends State<_ToastWidget>
|
||||
border: Border.all(color: config.borderColor),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.slate900.withValues(alpha: 0.08),
|
||||
color: colorScheme.shadow.withValues(alpha: 0.08),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/l10n/l10n.dart';
|
||||
import 'toast_type.dart';
|
||||
import '../../../core/theme/design_tokens.dart';
|
||||
|
||||
class ToastTypeConfig {
|
||||
final Color surfaceColor;
|
||||
@@ -22,36 +21,38 @@ class ToastTypeConfig {
|
||||
|
||||
static ToastTypeConfig fromType(BuildContext context, ToastType type) {
|
||||
final l10n = context.l10n;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return switch (type) {
|
||||
ToastType.success => ToastTypeConfig(
|
||||
surfaceColor: AppColors.feedbackSuccessSurface,
|
||||
borderColor: AppColors.feedbackSuccessBorder,
|
||||
iconColor: AppColors.feedbackSuccessIcon,
|
||||
textColor: AppColors.feedbackSuccessText,
|
||||
surfaceColor: colorScheme.tertiaryContainer,
|
||||
borderColor: colorScheme.tertiary,
|
||||
iconColor: colorScheme.tertiary,
|
||||
textColor: colorScheme.onTertiaryContainer,
|
||||
label: l10n.toastLabelSuccess,
|
||||
icon: Icons.check_circle_outline,
|
||||
),
|
||||
ToastType.warning => ToastTypeConfig(
|
||||
surfaceColor: AppColors.feedbackWarningSurface,
|
||||
borderColor: AppColors.feedbackWarningBorder,
|
||||
iconColor: AppColors.feedbackWarningIcon,
|
||||
textColor: AppColors.feedbackWarningText,
|
||||
surfaceColor: colorScheme.secondaryContainer,
|
||||
borderColor: colorScheme.secondary,
|
||||
iconColor: colorScheme.secondary,
|
||||
textColor: colorScheme.onSecondaryContainer,
|
||||
label: l10n.toastLabelWarning,
|
||||
icon: Icons.warning_amber_rounded,
|
||||
),
|
||||
ToastType.error => ToastTypeConfig(
|
||||
surfaceColor: AppColors.feedbackErrorSurface,
|
||||
borderColor: AppColors.feedbackErrorBorder,
|
||||
iconColor: AppColors.feedbackErrorIcon,
|
||||
textColor: AppColors.feedbackErrorText,
|
||||
surfaceColor: colorScheme.errorContainer,
|
||||
borderColor: colorScheme.error,
|
||||
iconColor: colorScheme.error,
|
||||
textColor: colorScheme.onErrorContainer,
|
||||
label: l10n.toastLabelError,
|
||||
icon: Icons.error_outline,
|
||||
),
|
||||
ToastType.info => ToastTypeConfig(
|
||||
surfaceColor: AppColors.feedbackInfoSurface,
|
||||
borderColor: AppColors.feedbackInfoBorder,
|
||||
iconColor: AppColors.feedbackInfoIcon,
|
||||
textColor: AppColors.feedbackInfoText,
|
||||
surfaceColor: colorScheme.primaryContainer,
|
||||
borderColor: colorScheme.primary,
|
||||
iconColor: colorScheme.primary,
|
||||
textColor: colorScheme.onPrimaryContainer,
|
||||
label: l10n.toastLabelInfo,
|
||||
icon: Icons.info_outline,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user