import 'package:flutter/material.dart'; import '../ui/app_icons.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, // 글꼴을 지정하지 않은 글자도 Figma 본문 글꼴(42dot Sans)로 그린다. fontFamily: AppFonts.body, // Figma Iconset(Lucide) — 뒤로가기 chevron-left · 닫기 x. actionIconTheme: ActionIconThemeData( backButtonIconBuilder: (_) => const ImageIcon(AppIcons.chevronLeft, size: 24), closeButtonIconBuilder: (_) => const ImageIcon(AppIcons.x, size: 24), ), scaffoldBackgroundColor: AppColors.cosmicVoid, textTheme: _textTheme(scheme), // Figma Top Navigation(알림 229:11265 · 설정 354:222008 등) — 높이 56, 바탕 #141414, // 제목 42dot Sans 16/600 가운데, 아이콘 24 를 56×56 칸에. appBarTheme: AppBarTheme( backgroundColor: AppColors.cosmicVoid, foregroundColor: AppColors.onCosmic, surfaceTintColor: Colors.transparent, elevation: 0, scrolledUnderElevation: 0, centerTitle: true, toolbarHeight: 56, titleTextStyle: _style(AppType.navTitle, AppColors.onCosmic), iconTheme: const IconThemeData(color: AppColors.onCosmic, size: 24), actionsIconTheme: const IconThemeData(color: AppColors.onCosmic, size: 24), ), // 시안 Bottom Navigation(350:215853) — 바탕 #141414, 알약 표시자 없음, 아이콘 24, // 라벨 ABeeZee 10/16. 활성은 흰색, 비활성은 #777777. 탭 칸 높이 64(홈 인디케이터 34 별도). navigationBarTheme: NavigationBarThemeData( backgroundColor: AppColors.cosmicVoid, surfaceTintColor: Colors.transparent, indicatorColor: Colors.transparent, height: 64, iconTheme: WidgetStateProperty.resolveWith( (states) => IconThemeData( size: 24, color: states.contains(WidgetState.selected) ? AppColors.onCosmic : AppColors.onCosmicMuted, ), ), labelTextStyle: WidgetStateProperty.resolveWith( (states) => _style( AppType.navLabel, states.contains(WidgetState.selected) ? AppColors.onCosmic : AppColors.onCosmicMuted, ), ), ), dividerTheme: const DividerThemeData(color: AppColors.cosmicOutline, thickness: 1), // 시안 행성 상세(373:441117) 탭 — 활성 청록 글자·밑줄, 비활성 #bbbbbb, 트랙 #1a1a1a. tabBarTheme: TabBarThemeData( labelColor: AppColors.cosmicTeal, unselectedLabelColor: const Color(0xFFBBBBBB), indicatorColor: AppColors.cosmicTeal, indicatorSize: TabBarIndicatorSize.tab, dividerColor: const Color(0xFF1A1A1A), labelStyle: _styleNoColor(AppType.labelL), unselectedLabelStyle: _styleNoColor(AppType.labelL), ), // ── 아래는 테마를 주지 않으면 Material 3 기본값(보라 시드)으로 칠해지는 것들이다. // 코스믹 다크 위에서 눈에 띄게 어긋나므로 전부 토큰으로 고정한다. // Figma 정렬 칩(Inside_최신순 196:2673) — 바탕 #1a1a1c · 테두리 #232324 · 모서리 8 · // 안쪽 6/10 · 글자 12/500. 선택은 흰 바탕에 검은 글자다. chipTheme: ChipThemeData( backgroundColor: AppColors.cosmicSurfaceContainer, selectedColor: AppColors.onCosmic, side: const BorderSide(color: AppColors.cosmicSurfaceRaised), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.sm), ), // 선택(흰 바탕)이면 검은 글자 — 상태 색을 주지 않으면 선택 칩도 연회색 글자라 읽히지 않는다(10/6 실기기). labelStyle: _style( AppType.caption, WidgetStateColor.resolveWith( (s) => s.contains(WidgetState.selected) ? Colors.black : const Color(0xFFD5D7D9), ), ), secondaryLabelStyle: _style(AppType.caption, AppColors.onCosmicAccent), checkmarkColor: AppColors.onCosmicAccent, showCheckmark: false, labelPadding: EdgeInsets.zero, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), ), cardTheme: CardThemeData( color: AppColors.cosmicSurfaceContainer, surfaceTintColor: Colors.transparent, elevation: 0, margin: EdgeInsets.zero, // Figma 카드(MY 373:632962 등 36곳) — 모서리 16. shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.lg), ), ), 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, ), // Figma 액션 시트(343:212939) #2C2C2E · 모서리 16. bottomSheetTheme: BottomSheetThemeData( backgroundColor: AppColors.cosmicSheet, surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.lg)), ), ), // Figma 확인 팝업(차단하기 373:442417) #2D2D2D · 모서리 12. dialogTheme: DialogThemeData( backgroundColor: AppColors.cosmicDialog, surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.md), ), ), snackBarTheme: SnackBarThemeData( backgroundColor: AppColors.cosmicSurfaceRaised, contentTextStyle: _style(AppType.bodyM, AppColors.onCosmic), // 「보기」 등 액션 글자 — 기본(inversePrimary)은 어두운 바탕에 묻힌다(10/6 실기기). 강조색 토큰. actionTextColor: AppColors.cosmicAccent, 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(_buttonHeight), padding: _buttonPadding, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.sm), ), textStyle: _styleNoColor(AppType.navTitle), ), ), iconTheme: const IconThemeData(color: AppColors.onCosmicVariant), filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(_buttonHeight), padding: _buttonPadding, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.sm), ), // 글자색은 여기서 정하지 않는다 — foregroundColor 가 결정한다. // textStyle 에 색을 넣으면 소셜 버튼처럼 제공자별 색을 쓰는 곳과 지정이 두 곳으로 갈린다. textStyle: _styleNoColor(AppType.navTitle), ), ), ); } /// Figma 주 버튼(퀘스트 상세 264:48835 등) — 343×56, 모서리 8, 안쪽 16/24, 글자 16/600. const _buttonHeight = 56.0; const _buttonPadding = EdgeInsets.symmetric(horizontal: 24, vertical: 16); 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), titleSmall: _style(AppType.labelL, 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, // ABeeZee 는 라틴 전용이다. 한글은 본문 글꼴로 이어 그린다(Figma 도 같은 대체를 한다). fontFamilyFallback: const [AppFonts.body], 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)];