테마 — Material 컴포넌트 12종을 디자인 토큰으로 고정 · 테마 적용 범위 테스트
This commit is contained in:
@@ -16,6 +16,14 @@ ThemeData buildAppTheme() {
|
|||||||
onPrimary: AppColors.onCosmicAccent,
|
onPrimary: AppColors.onCosmicAccent,
|
||||||
secondary: AppColors.neonViolet,
|
secondary: AppColors.neonViolet,
|
||||||
onSecondary: AppColors.onNeonViolet,
|
onSecondary: AppColors.onNeonViolet,
|
||||||
|
// 컨테이너 슬롯을 비워 두면 Material 이 기본 시드(보라)에서 값을 만들어 낸다.
|
||||||
|
// ChoiceChip 의 선택 상태가 그 색을 쓰므로 코스믹 다크 위에 남색 칩이 뜬다.
|
||||||
|
primaryContainer: AppColors.cosmicSurfaceRaised,
|
||||||
|
onPrimaryContainer: AppColors.cosmicAccent,
|
||||||
|
secondaryContainer: AppColors.cosmicSurfaceRaised,
|
||||||
|
onSecondaryContainer: AppColors.cosmicAccent,
|
||||||
|
tertiaryContainer: AppColors.cosmicSurfaceRaised,
|
||||||
|
onTertiaryContainer: AppColors.neonCyan,
|
||||||
tertiary: AppColors.neonCyan,
|
tertiary: AppColors.neonCyan,
|
||||||
onTertiary: AppColors.onNeonCyan,
|
onTertiary: AppColors.onNeonCyan,
|
||||||
error: AppColors.error,
|
error: AppColors.error,
|
||||||
@@ -58,6 +66,110 @@ ThemeData buildAppTheme() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
dividerTheme: const DividerThemeData(color: AppColors.cosmicOutline, thickness: 1),
|
dividerTheme: const DividerThemeData(color: AppColors.cosmicOutline, thickness: 1),
|
||||||
|
|
||||||
|
// ── 아래는 테마를 주지 않으면 Material 3 기본값(보라 시드)으로 칠해지는 것들이다.
|
||||||
|
// 코스믹 다크 위에서 눈에 띄게 어긋나므로 전부 토큰으로 고정한다.
|
||||||
|
chipTheme: ChipThemeData(
|
||||||
|
backgroundColor: AppColors.cosmicSurfaceContainer,
|
||||||
|
selectedColor: AppColors.cosmicSurfaceRaised,
|
||||||
|
// 선택 상태는 색면이 아니라 테두리와 글자색으로 알린다 — 악센트는 행동에만 쓴다.
|
||||||
|
side: const BorderSide(color: AppColors.cosmicOutline),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||||
|
),
|
||||||
|
labelStyle: _style(AppType.labelL, AppColors.onCosmicVariant),
|
||||||
|
secondaryLabelStyle: _style(AppType.labelL, AppColors.cosmicAccent),
|
||||||
|
checkmarkColor: AppColors.cosmicAccent,
|
||||||
|
showCheckmark: false,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: AppSpace.s3,
|
||||||
|
vertical: AppSpace.s2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cardTheme: CardThemeData(
|
||||||
|
color: AppColors.cosmicSurfaceContainer,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||||
|
backgroundColor: AppColors.cosmicAccent,
|
||||||
|
foregroundColor: AppColors.onCosmicAccent,
|
||||||
|
elevation: 0,
|
||||||
|
extendedTextStyle: _styleNoColor(AppType.titleM),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
// 채워진 상자 대신 밑줄을 쓴다. 어두운 서피스에서 상자를 겹치면 층이 흐려진다.
|
||||||
|
labelStyle: _style(AppType.bodyM, AppColors.onCosmicVariant),
|
||||||
|
floatingLabelStyle: _style(AppType.caption, AppColors.cosmicAccent),
|
||||||
|
helperStyle: _style(AppType.caption, AppColors.onCosmicVariant),
|
||||||
|
counterStyle: _style(AppType.caption, AppColors.onCosmicVariant),
|
||||||
|
enabledBorder: const UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: AppColors.cosmicOutline),
|
||||||
|
),
|
||||||
|
focusedBorder: const UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: AppColors.cosmicAccent, width: 2),
|
||||||
|
),
|
||||||
|
errorBorder: const UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: AppColors.error),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
progressIndicatorTheme: const ProgressIndicatorThemeData(
|
||||||
|
color: AppColors.cosmicAccent,
|
||||||
|
linearTrackColor: AppColors.cosmicSurfaceRaised,
|
||||||
|
circularTrackColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
bottomSheetTheme: BottomSheetThemeData(
|
||||||
|
backgroundColor: AppColors.cosmicSurfaceContainer,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.lg)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dialogTheme: DialogThemeData(
|
||||||
|
backgroundColor: AppColors.cosmicSurfaceContainer,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
snackBarTheme: SnackBarThemeData(
|
||||||
|
backgroundColor: AppColors.cosmicSurfaceRaised,
|
||||||
|
contentTextStyle: _style(AppType.bodyM, AppColors.onCosmic),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
listTileTheme: ListTileThemeData(
|
||||||
|
iconColor: AppColors.onCosmicVariant,
|
||||||
|
titleTextStyle: _style(AppType.bodyL, AppColors.onCosmic),
|
||||||
|
subtitleTextStyle: _style(AppType.bodyS, AppColors.onCosmicVariant),
|
||||||
|
),
|
||||||
|
textButtonTheme: TextButtonThemeData(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: AppColors.cosmicAccent,
|
||||||
|
textStyle: _styleNoColor(AppType.titleM),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: AppColors.onCosmic,
|
||||||
|
side: const BorderSide(color: AppColors.cosmicOutline),
|
||||||
|
minimumSize: const Size.fromHeight(AppSizing.touchMinApp),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||||
|
),
|
||||||
|
textStyle: _styleNoColor(AppType.titleM),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
iconTheme: const IconThemeData(color: AppColors.onCosmicVariant),
|
||||||
filledButtonTheme: FilledButtonThemeData(
|
filledButtonTheme: FilledButtonThemeData(
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
minimumSize: const Size.fromHeight(AppSizing.touchMinApp),
|
minimumSize: const Size.fromHeight(AppSizing.touchMinApp),
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import 'package:fancake/core/theme/app_theme.dart';
|
||||||
|
import 'package:fancake/core/theme/tokens.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
/// 테마를 주지 않은 컴포넌트는 Material 3 가 기본 시드(보라)에서 색을 만들어 낸다.
|
||||||
|
///
|
||||||
|
/// 코스믹 다크 서피스 위에 그 색이 뜨면 시안과 어긋나는데, 화면을 눈으로 보기
|
||||||
|
/// 전까지 드러나지 않는다. 실제로 INSIDE·STATION 정렬 칩이 보라색으로 나간 적이
|
||||||
|
/// 있어(09-02) 그 회귀를 여기서 잡는다.
|
||||||
|
void main() {
|
||||||
|
final theme = buildAppTheme();
|
||||||
|
|
||||||
|
/// 코스믹 팔레트에 속한 색인가. 토큰 밖의 색이면 Material 이 만들어 낸 것이다.
|
||||||
|
bool isCosmic(Color? c) {
|
||||||
|
if (c == null) return false;
|
||||||
|
final palette = <Color>{
|
||||||
|
AppColors.cosmicVoid,
|
||||||
|
AppColors.cosmicSurface,
|
||||||
|
AppColors.cosmicSurfaceContainer,
|
||||||
|
AppColors.cosmicSurfaceRaised,
|
||||||
|
AppColors.cosmicOutline,
|
||||||
|
AppColors.cosmicAccent,
|
||||||
|
AppColors.onCosmic,
|
||||||
|
AppColors.onCosmicVariant,
|
||||||
|
AppColors.onCosmicAccent,
|
||||||
|
AppColors.neonCyan,
|
||||||
|
AppColors.neonViolet,
|
||||||
|
AppColors.error,
|
||||||
|
};
|
||||||
|
return palette.contains(c) || c == Colors.transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
group('테마가 컴포넌트를 비워 두지 않는다', () {
|
||||||
|
test('Material 기본값으로 떨어지던 12종에 테마가 있다', () {
|
||||||
|
expect(theme.chipTheme.backgroundColor, isNotNull, reason: 'ChipTheme');
|
||||||
|
expect(theme.cardTheme.color, isNotNull, reason: 'CardTheme');
|
||||||
|
expect(theme.floatingActionButtonTheme.backgroundColor, isNotNull, reason: 'FAB');
|
||||||
|
expect(theme.inputDecorationTheme.focusedBorder, isNotNull, reason: 'Input');
|
||||||
|
expect(theme.progressIndicatorTheme.color, isNotNull, reason: 'Progress');
|
||||||
|
expect(theme.bottomSheetTheme.backgroundColor, isNotNull, reason: 'BottomSheet');
|
||||||
|
expect(theme.dialogTheme.backgroundColor, isNotNull, reason: 'Dialog');
|
||||||
|
expect(theme.snackBarTheme.backgroundColor, isNotNull, reason: 'SnackBar');
|
||||||
|
expect(theme.listTileTheme.titleTextStyle, isNotNull, reason: 'ListTile');
|
||||||
|
expect(theme.textButtonTheme.style, isNotNull, reason: 'TextButton');
|
||||||
|
expect(theme.outlinedButtonTheme.style, isNotNull, reason: 'OutlinedButton');
|
||||||
|
expect(theme.iconTheme.color, isNotNull, reason: 'IconTheme');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('칩 색이 코스믹 팔레트 안에 있다', () {
|
||||||
|
// 보라색 칩이 다시 나오면 여기서 잡힌다.
|
||||||
|
expect(isCosmic(theme.chipTheme.backgroundColor), isTrue);
|
||||||
|
expect(isCosmic(theme.chipTheme.selectedColor), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ColorScheme 의 컨테이너 슬롯이 채워져 있다', () {
|
||||||
|
// 비워 두면 Material 이 시드에서 만들어 내고, 그 값이 칩·배지로 샌다.
|
||||||
|
final s = theme.colorScheme;
|
||||||
|
for (final (name, c) in [
|
||||||
|
('primaryContainer', s.primaryContainer),
|
||||||
|
('secondaryContainer', s.secondaryContainer),
|
||||||
|
('tertiaryContainer', s.tertiaryContainer),
|
||||||
|
]) {
|
||||||
|
expect(isCosmic(c), isTrue, reason: '$name 이 팔레트 밖이다: $c');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user