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, ), // 시안 Bottom Navigation(350:215853) — 바탕 #141414, 알약 표시자 없음, 아이콘 24, // 라벨 ABeeZee 10. 활성은 흰색, 비활성은 #777777. navigationBarTheme: NavigationBarThemeData( backgroundColor: AppColors.cosmicVoid, surfaceTintColor: Colors.transparent, indicatorColor: Colors.transparent, // 탭 항목 높이는 touch-min-app(48) 아래로 내리지 않는다. height: AppSizing.touchMinApp + AppSpace.s5, iconTheme: WidgetStateProperty.resolveWith( (states) => IconThemeData( size: 24, color: states.contains(WidgetState.selected) ? AppColors.onCosmic : AppColors.onCosmicMuted, ), ), labelTextStyle: WidgetStateProperty.resolveWith( (states) => TextStyle( fontFamily: AppFonts.display, fontSize: 10, letterSpacing: 0.2, color: states.contains(WidgetState.selected) ? AppColors.onCosmic : AppColors.onCosmicMuted, ), ), ), 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)];