import 'package:flutter/material.dart'; import '../../../core/theme/tokens.dart'; import '../../../core/ui/app_icons.dart'; import '../../../core/ui/relative_time.dart' show formatReaction; import '../../home/home_screen.dart' show formatPop; import '../my_data.dart'; import '../../../core/i18n/app_locale.dart'; /// MY 시안(`373:632962` 외 F6 전체)에서 되풀이되는 색. 토큰에 없는 Figma 값만 둔다. abstract final class MyColors { /// 카드 바탕 #19191c. static const card = Color(0xFF19191C); /// 카드 안 구분선 #2c2c2c. static const divider = Color(0xFF2C2C2C); /// 행 끝 chevron #444444. static const chevron = Color(0xFF444444); /// 보조 값(닉네임 · 이메일 오른쪽) #bbbbbb. static const value = Color(0xFFBBBBBB); /// 섹션 머리 #999999. static const section = Color(0xFF999999); /// 통계 라벨 #777777. static const muted = Color(0xFF777777); /// 위험 행(로그아웃) #ff5e5e. static const danger = Color(0xFFFF5E5E); } /// 42dot Sans 스타일 한 줄. Figma 자간 -2.5% 를 크기에 맞춰 넣는다. TextStyle myText( double size, FontWeight weight, { Color color = AppColors.onCosmic, double height = 1.4, }) => TextStyle( fontFamily: AppFonts.body, fontSize: size, fontWeight: weight, height: height, letterSpacing: size * -0.025, color: color, ); /// 행 끝 chevron-right 24. class MyChevron extends StatelessWidget { const MyChevron({super.key}); @override Widget build(BuildContext context) => const ImageIcon(AppIcons.chevronRight, size: 24, color: MyColors.chevron); } /// 티어 카드 — 시안 MY `373:632962` 「GLOW TIER ⓘ · 진행 바 · 누적 기여 / 값 · ›」 틀. /// /// C-18 — 제목 자리에 User Level(`Lv.N` + 칭호)을, 그 옆에 Passport Tier(B-65)를 /// 따로 적는다(FR-MY-01 세 체계 구분). 진행 바는 레벨 구간 EXP 진행도다. /// [onInfo] · [onDetail] 이 없으면(프로필 카드 오버레이) 아이콘과 화살표를 그리지 않는다. class MyTierCard extends StatelessWidget { const MyTierCard({ super.key, required this.profile, this.onInfo, this.onDetail, }); final MyProfile profile; final VoidCallback? onInfo; final VoidCallback? onDetail; @override Widget build(BuildContext context) => GestureDetector( behavior: HitTestBehavior.opaque, onTap: onDetail, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: MyColors.card, borderRadius: BorderRadius.circular(AppRadius.lg), ), child: Row( children: [ Image.asset( 'assets/figma/icon-tier-sprout.png', width: 60, height: 60, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Flexible( child: Text( profile.levelLabel, style: myText(14, FontWeight.w500), overflow: TextOverflow.ellipsis, ), ), // Passport Tier 는 레벨과 별개 체계다(B-65 · B-66). Text( ' · ${profile.passportTier.label}', style: myText(14, FontWeight.w500, color: MyColors.value), ), if (onInfo != null) ...[ const SizedBox(width: 4), Semantics( button: true, label: context.l10n.tierGuide, child: GestureDetector( onTap: onInfo, child: const ImageIcon( AssetImage('assets/figma/icon-info.png'), size: 14, color: MyColors.value, ), ), ), ], ], ), const SizedBox(height: 8), MyProgressBar(value: profile.levelProgress), const SizedBox(height: 8), Row( children: [ Text( 'EXP', style: myText( 12, FontWeight.w500, color: AppColors.cosmicAccent, ), ), const Spacer(), Text( formatPop(profile.userExp), style: myText( 12, FontWeight.w500, color: AppColors.cosmicAccent, ), ), ], ), ], ), ), if (onDetail != null) ...[ const SizedBox(width: 12), const MyChevron(), ], ], ), ), ); } /// 진행 바 — 높이 10 · 트랙 검정 20% · 채움 분홍 방사 그라데이션(시안 Rectangle 32988). class MyProgressBar extends StatelessWidget { const MyProgressBar({super.key, required this.value}); final double value; @override Widget build(BuildContext context) => ClipRRect( borderRadius: BorderRadius.circular(AppRadius.full), child: SizedBox( height: 10, child: Stack( children: [ const Positioned.fill(child: ColoredBox(color: Color(0x33000000))), FractionallySizedBox( widthFactor: value.clamp(0.0, 1.0), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppRadius.full), gradient: const RadialGradient( radius: 4, colors: [ Color(0xFFFF7DDD), Color(0xFFFF97E4), Color(0xFFFF7DDD), ], stops: [0, 0.7, 1], ), ), ), ), ], ), ), ); } /// 통계 카드 — 시안 MY: 3D 아이콘 36 · 라벨 12/600 #777 · 값 16/600. class MyStatsCard extends StatelessWidget { const MyStatsCard({super.key, required this.profile}); final MyProfile profile; @override Widget build(BuildContext context) => Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), decoration: BoxDecoration( color: MyColors.card, borderRadius: BorderRadius.circular(AppRadius.lg), ), // 세 칸을 고르게 나눈다. 시안의 칸 안쪽 20 을 그대로 두면 여섯 자리 수치에서 넘친다. child: Row( children: [ // 시안 표기 「1.0K」 — 걸음은 K 단위로 줄인다. POP 은 천 단위 구분자다. _Stat( icon: 'assets/figma/icon-steps.png', label: context.l10n.statSteps, value: formatReaction(profile.steps), ), _Stat( icon: 'assets/figma/icon-pop-bolt.png', label: context.l10n.statContribution, value: formatPop(profile.contributedPop), ), _Stat( icon: 'assets/figma/icon-quests.png', label: context.l10n.statQuests, value: '${profile.completedQuests}', ), ], ), ); } class _Stat extends StatelessWidget { const _Stat({required this.icon, required this.label, required this.value}); final String icon; final String label; final String value; @override Widget build(BuildContext context) => Expanded( child: Column( children: [ Image.asset(icon, width: 36, height: 36), const SizedBox(height: 6), Text(label, style: myText(12, FontWeight.w600, color: MyColors.muted)), const SizedBox(height: 12), Text(value, style: myText(16, FontWeight.w600)), ], ), ); } /// 두 칸 탭 — 시안 「행성/성운 · 활동」: 칸 높이 44 · 글자 16/500 · 활성 흰 밑줄, /// 비활성 #1a1a1a 밑줄 · 비활성 글자 #bbbbbb. class MyTabBar extends StatelessWidget { const MyTabBar({super.key, required this.tabs}); final List tabs; @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: TabBar( labelColor: AppColors.onCosmic, unselectedLabelColor: MyColors.value, indicator: const UnderlineTabIndicator( borderSide: BorderSide(color: AppColors.onCosmic, width: 2), ), indicatorSize: TabBarIndicatorSize.tab, dividerColor: const Color(0xFF1A1A1A), dividerHeight: 2, labelPadding: EdgeInsets.zero, labelStyle: myText(16, FontWeight.w500, height: 1), unselectedLabelStyle: myText(16, FontWeight.w500, height: 1), tabs: [for (final t in tabs) Tab(height: 44, text: t)], ), ); } /// 정렬 칩 — 시안 Chip(`370:228289`): 높이 32 · 모서리 8 · 아이콘 16 · 글자 12/500. /// 선택은 흰 바탕에 검정 글자, 비선택은 #1a1a1a 바탕 · #232324 획 · #d5d7d9 글자. class MySortChip extends StatelessWidget { const MySortChip({ super.key, required this.label, required this.icon, required this.selected, required this.onTap, }); static const clock = AssetImage('assets/figma/icon-clock.png'); static const bolt = AssetImage('assets/figma/icon-pop-small.png'); static const fileCheck = AssetImage('assets/figma/icon-file-check.png'); final String label; final ImageProvider icon; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) { final fg = selected ? Colors.black : const Color(0xFFD5D7D9); return GestureDetector( onTap: onTap, child: Container( height: 32, padding: const EdgeInsets.symmetric(horizontal: 10), decoration: BoxDecoration( color: selected ? AppColors.onCosmic : const Color(0xFF1A1A1A), borderRadius: BorderRadius.circular(AppRadius.sm), border: Border.all(color: AppColors.cosmicSurfaceRaised), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ ImageIcon(icon, size: 16, color: fg), const SizedBox(width: 4), Text( label, style: myText(12, FontWeight.w500, color: fg, height: 1.3), ), ], ), ), ); } } /// 섹션 — 머리 12/600 #999 · 간격 16 · 카드 #19191c 모서리 16 · 안쪽 20/16 · 줄 간격 16. /// 섹션 사이는 24. [title] 이 없으면 카드만 그린다(로그아웃 · 탈퇴). class MyGroup extends StatelessWidget { const MyGroup({this.title, required this.children}); final String? title; final List children; @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.only(bottom: 24), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (title case final t?) ...[ Text( t, style: myText( 12, FontWeight.w600, color: MyColors.section, height: 1, ), ), const SizedBox(height: 16), ], Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), decoration: BoxDecoration( color: MyColors.card, borderRadius: BorderRadius.circular(AppRadius.lg), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (final (i, c) in children.indexed) ...[ if (i > 0) const SizedBox(height: 16), c, ], ], ), ), ], ), ); } class MyDivider extends StatelessWidget { const MyDivider(); @override Widget build(BuildContext context) => const SizedBox(height: 1, child: ColoredBox(color: MyColors.divider)); } /// 눌러서 들어가는 줄 — 높이 24 · 글자 16/600 · 끝 chevron(언어는 아래 화살표). class MyLinkRow extends StatelessWidget { const MyLinkRow({ this.label, this.child, this.color = AppColors.onCosmic, this.down = false, required this.onTap, }); final String? label; final Widget? child; final Color color; final bool down; final VoidCallback onTap; @override Widget build(BuildContext context) => GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, child: SizedBox( height: child == null ? 24 : 25, child: Row( children: [ Expanded( child: child ?? Text( label!, style: myText(16, FontWeight.w600, color: color, height: 1), ), ), if (down) const ImageIcon( AssetImage('assets/figma/icon-chevron-down.png'), size: 24, color: Color(0xFF4F4F4F), ) else const MyChevron(), ], ), ), ); }