218 lines
9.2 KiB
Dart
218 lines
9.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'tokens.dart';
|
|
|
|
/// design-tokens.md → ThemeData 조립.
|
|
///
|
|
/// 값은 전부 [tokens.dart] 에서 온다. 여기서 색·크기를 새로 만들면 시안과 어긋난다.
|
|
/// 서피스는 코스믹 보이드(순수 검정)다 — 결정 D-19, `design/아트디렉션-예외.md`.
|
|
///
|
|
/// 컬러 운용은 60-30-10 이고 악센트는 행동·선택·포커스에만 쓴다.
|
|
/// ColorScheme 슬롯에 네온을 꽂아 뒀다고 해서 화면에서 네 색을 다 풀면 안 된다 —
|
|
/// 화면당 네온은 하나를 주로 하고 나머지는 보조로만(design-tokens.md 「컬러 운용 규율」).
|
|
ThemeData buildAppTheme() {
|
|
const scheme = ColorScheme.dark(
|
|
primary: AppColors.cosmicAccent,
|
|
onPrimary: AppColors.onCosmicAccent,
|
|
secondary: AppColors.neonViolet,
|
|
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,
|
|
onTertiary: AppColors.onNeonCyan,
|
|
error: AppColors.error,
|
|
onError: AppColors.onError,
|
|
surface: AppColors.cosmicVoid,
|
|
onSurface: AppColors.onCosmic,
|
|
onSurfaceVariant: AppColors.onCosmicVariant,
|
|
surfaceContainerLowest: AppColors.cosmicVoid,
|
|
surfaceContainerLow: AppColors.cosmicSurface,
|
|
surfaceContainer: AppColors.cosmicSurfaceContainer,
|
|
surfaceContainerHigh: AppColors.cosmicSurfaceRaised,
|
|
surfaceContainerHighest: AppColors.cosmicSurfaceRaised,
|
|
outline: AppColors.cosmicOutline,
|
|
outlineVariant: AppColors.cosmicOutline,
|
|
);
|
|
|
|
return ThemeData(
|
|
useMaterial3: true,
|
|
colorScheme: scheme,
|
|
scaffoldBackgroundColor: AppColors.cosmicVoid,
|
|
textTheme: _textTheme(scheme),
|
|
appBarTheme: const AppBarTheme(
|
|
backgroundColor: AppColors.cosmicVoid,
|
|
surfaceTintColor: Colors.transparent,
|
|
centerTitle: true,
|
|
),
|
|
navigationBarTheme: NavigationBarThemeData(
|
|
backgroundColor: AppColors.cosmicSurface,
|
|
surfaceTintColor: Colors.transparent,
|
|
indicatorColor: AppColors.cosmicSurfaceRaised,
|
|
// 탭 항목 높이는 touch-min-app(48) 아래로 내리지 않는다.
|
|
height: AppSizing.touchMinApp + AppSpace.s5,
|
|
labelTextStyle: WidgetStateProperty.resolveWith(
|
|
(states) => _style(
|
|
AppType.caption,
|
|
states.contains(WidgetState.selected)
|
|
? AppColors.cosmicAccent
|
|
: AppColors.onCosmicVariant,
|
|
),
|
|
),
|
|
),
|
|
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(
|
|
style: FilledButton.styleFrom(
|
|
minimumSize: const Size.fromHeight(AppSizing.touchMinApp),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.md),
|
|
),
|
|
// 글자색은 여기서 정하지 않는다 — foregroundColor 가 결정한다.
|
|
// textStyle 에 색을 넣으면 소셜 버튼처럼 제공자별 색을 쓰는 곳과 지정이 두 곳으로 갈린다.
|
|
textStyle: _styleNoColor(AppType.titleM),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
TextTheme _textTheme(ColorScheme scheme) => TextTheme(
|
|
displayLarge: _style(AppType.displayL, scheme.onSurface),
|
|
displayMedium: _style(AppType.displayM, scheme.onSurface),
|
|
headlineSmall: _style(AppType.titleL, scheme.onSurface),
|
|
titleLarge: _style(AppType.titleL, scheme.onSurface),
|
|
titleMedium: _style(AppType.titleM, scheme.onSurface),
|
|
bodyLarge: _style(AppType.bodyL, scheme.onSurface),
|
|
bodyMedium: _style(AppType.bodyM, scheme.onSurface),
|
|
bodySmall: _style(AppType.bodyS, scheme.onSurfaceVariant),
|
|
labelLarge: _style(AppType.labelL, scheme.onSurface),
|
|
labelMedium: _style(AppType.eyebrow, scheme.onSurfaceVariant),
|
|
labelSmall: _style(AppType.caption, scheme.onSurfaceVariant),
|
|
);
|
|
|
|
/// [TypeToken] → [TextStyle].
|
|
///
|
|
/// 폰트 파일 미투입 상태에서는 fontFamily 가 매칭되지 않아 시스템 폰트로 폴백된다.
|
|
/// 크기·굵기·행간·자간은 그대로 적용되므로 레이아웃은 투입 전후가 거의 같다.
|
|
TextStyle _style(TypeToken t, Color color) => _styleNoColor(t).copyWith(color: color);
|
|
|
|
/// 색 없는 타입 스타일. 색을 다른 속성(foregroundColor 등)이 정하는 자리에 쓴다.
|
|
TextStyle _styleNoColor(TypeToken t) => TextStyle(
|
|
fontFamily: t.family,
|
|
fontSize: t.size,
|
|
fontWeight: _weight(t.weight),
|
|
height: t.height,
|
|
letterSpacing: t.letterSpacing,
|
|
);
|
|
|
|
FontWeight _weight(int value) =>
|
|
FontWeight.values[(value ~/ 100 - 1).clamp(0, FontWeight.values.length - 1)];
|